feat: full pattern matrix — M1 complete, M2 LLM chat, 30+ pattern files #10

Open
pyr0ball wants to merge 27 commits from feat/patterns-expansion into main
49 changed files with 11087 additions and 79 deletions

3
.gitignore vendored
View file

@ -29,3 +29,6 @@ src-tauri/target/
# Secrets # Secrets
.env .env
.env.local .env.local
# Visual companion brainstorm sessions
.superpowers/

11
package-lock.json generated
View file

@ -8,6 +8,7 @@
"name": "robin", "name": "robin",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2.11.0",
"vue": "^3.5.34" "vue": "^3.5.34"
}, },
"devDependencies": { "devDependencies": {
@ -398,6 +399,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@tauri-apps/api": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.0.tgz",
"integrity": "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==",
"license": "Apache-2.0 OR MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/tauri"
}
},
"node_modules/@tybys/wasm-util": { "node_modules/@tybys/wasm-util": {
"version": "0.10.2", "version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",

View file

@ -9,6 +9,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2.11.0",
"vue": "^3.5.34" "vue": "^3.5.34"
}, },
"devDependencies": { "devDependencies": {

6158
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -24,8 +24,14 @@ log = "0.4"
anyhow = "1.0" anyhow = "1.0"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
toml = "0.8" toml = "0.8"
notify = "8"
dirs = "6"
tauri = { version = "2.11.2", features = ["tray-icon"] } tauri = { version = "2.11.2", features = ["tray-icon"] }
tauri-plugin-log = "2" tauri-plugin-log = "2"
tauri-plugin-notification = "2" tauri-plugin-notification = "2"
tauri-plugin-shell = "2" tauri-plugin-shell = "2"
tauri-plugin-fs = "2" tauri-plugin-fs = "2"
reqwest = { version = "0.12", features = ["json", "stream"] }
[dev-dependencies]
tempfile = "3"

View file

@ -3,9 +3,11 @@
"identifier": "default", "identifier": "default",
"description": "enables the default permissions", "description": "enables the default permissions",
"windows": [ "windows": [
"main" "chat"
], ],
"permissions": [ "permissions": [
"core:default" "core:default",
"notification:default",
"shell:allow-open"
] ]
} }

View file

@ -0,0 +1,136 @@
[meta]
source_os = "android"
target_distro_family = "arch"
# Android user on their first Arch/CachyOS install.
# Assumes NO terminal experience (unless they used Termux).
# Every explanation starts from first principles.
# App Store analogy: pacman/AUR = Google Play + sideloading.
[log_paths]
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
# ── Package management ────────────────────────────────────────────────────────
[[patterns]]
id = "pacman-db-lock"
sources = ["journald", "applog:pacman"]
match_text = "could not lock database: File exists"
severity = "warn"
title = "App installer is busy"
body = "The package manager (Linux's equivalent of the Play Store) got interrupted and left a lock file. A lock file is a signal to other processes that says 'I'm running, don't start.' If nothing is installing right now, remove it: open a terminal, type exactly: sudo rm /var/lib/pacman/db.lck — then press Enter and try again. 'sudo' means 'run this as administrator.'"
[[patterns]]
id = "partial-upgrade-warning"
sources = ["applog:pacman"]
match_text = "warning: database file for"
severity = "info"
title = "App list is out of date — update everything"
body = "On Android, updates happen automatically. On Arch Linux, you need to run updates manually — and there's an important rule: always update ALL apps at the same time, never just the list. In a terminal, type: sudo pacman -Syu — then press Enter. Enter your password when asked. The -Syu means 'sync the list AND upgrade everything.'"
[[patterns]]
id = "pacman-dep-conflict"
sources = ["journald", "applog:pacman"]
match_text = "conflicting dependencies"
severity = "warn"
title = "Two apps conflict with each other"
body = "Two packages need something that can't be shared — like two apps that both want to be the default music player. Read the message carefully. Usually one package replaces another. Remove the old one first: sudo pacman -R <old-package-name> — then try your install again."
[[patterns]]
id = "aur-build-failure"
sources = ["journald", "applog:pacman"]
match_text = "error: failed to build"
severity = "warn"
title = "App build failed (AUR)"
body = "The AUR is like sideloading apps on Android — you're installing from source code that gets compiled on your machine, not a pre-built app. The build failed, usually because of a missing tool or broken code. Look at the error text above this message for the specific reason. The AUR package's comments page on aur.archlinux.org often has fixes."
# ── Terminal basics ───────────────────────────────────────────────────────────
[[patterns]]
id = "command-not-found"
sources = ["journald"]
match_text = "command not found"
severity = "info"
title = "Command not found — app may not be installed"
body = "You tried to run a program that isn't installed. On Android, apps are visible in the drawer; on Linux, they're invisible until you ask for them. To find and install the missing program, try: sudo pacman -Ss <name> — this searches for it. Then install with: sudo pacman -S <name>."
[[patterns]]
id = "permission-denied"
sources = ["journald"]
match_text = "Permission denied"
severity = "info"
title = "Permission denied"
body = "Linux has a permission system where files and folders are owned by specific users. This is more visible here than on Android. If you need admin access for a command, put 'sudo' before it — like: sudo <command>. For files you own but can't access, check ownership with: ls -la /path/to/file"
# ── System ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Hardware driver file missing"
body = "On Android, drivers come pre-installed and invisible. On Linux, some hardware needs a separate firmware file — like a plugin for your Wi-Fi chip or graphics card. Install the main firmware package: sudo pacman -S linux-firmware — then restart. If a specific device still doesn't work, the error message above will name which firmware file is missing."
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "System ran out of memory — closed an app"
body = "Linux ran out of RAM and had to close a program, similar to Android killing background apps. If this keeps happening, close programs you're not using, or add 'swap' (disk space used as overflow RAM): sudo pacman -S zram-generator"
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Storage error"
body = "Something went wrong reading or writing to the drive. This could be a hardware problem. Check drive health: sudo smartctl -a /dev/sda — first install the tool: sudo pacman -S smartmontools"
# ── Audio ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "Sound system not responding"
body = "PipeWire is the audio manager — like the sound settings system inside Android. Restart it: open a terminal and type: systemctl --user restart pipewire pipewire-pulse wireplumber — if sound still doesn't work, log out and log back in."
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth blocked by software switch"
body = "A software setting is blocking Bluetooth — like Airplane Mode on your phone. Run: rfkill unblock bluetooth — in a terminal. If it shows 'Hard blocked', there's a physical switch or BIOS setting to check."
[[patterns]]
id = "bluetooth-profile-unavailable"
sources = ["journald"]
match_text = "br-connection-profile-unavailable"
severity = "info"
title = "Bluetooth audio profile missing"
body = "Your Bluetooth device connected but the right audio mode isn't available. Install: sudo pacman -S pipewire-bluetooth — then restart Bluetooth: sudo systemctl restart bluetooth"
# ── Network ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "Wi-Fi connection failed"
body = "Linux couldn't connect to the network. Common causes: wrong password, or the Wi-Fi driver isn't loaded. Check connection status: nmcli device status — if the Wi-Fi adapter doesn't appear, the driver may need to be installed."
# ── GPU ───────────────────────────────────────────────────────────────────────
[[patterns]]
id = "gpu-hang"
sources = ["kmsg"]
match_text = "GPU HANG"
severity = "warn"
title = "Graphics card stopped responding"
body = "The graphics card froze and the driver recovered it — like a forced restart of the GPU. Games or video apps may have crashed. If this keeps happening, check that your graphics drivers are current: sudo pacman -Syu"

View file

@ -0,0 +1,137 @@
[meta]
source_os = "android"
target_distro_family = "debian"
# Android user on their first Debian/Ubuntu/Mint install.
# Assumes NO terminal experience. Ubuntu/Mint are the recommended starting points
# for Android migrants because of automatic updates and GUI app stores (GNOME Software/Discover).
[log_paths]
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
# ── Package management ────────────────────────────────────────────────────────
[[patterns]]
id = "apt-lock"
sources = ["journald"]
match_text = "Could not get lock /var/lib/dpkg/lock"
severity = "warn"
title = "App installer is busy"
body = "The software installer (Ubuntu/Mint calls it 'apt') is already running — probably doing automatic background updates, similar to how Android apps update silently. Wait a minute and try again. If it's stuck: open a terminal and type: sudo rm /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock — then: sudo dpkg --configure -a"
[[patterns]]
id = "dpkg-interrupted"
sources = ["journald"]
match_text = "dpkg was interrupted"
severity = "warn"
title = "App install was cut short"
body = "A previous install didn't finish cleanly — like pulling the charging cable out mid-update on your phone. Fix it: open a terminal and type: sudo dpkg --configure -a — then try your install again."
[[patterns]]
id = "apt-unmet-dependency"
sources = ["journald"]
match_text = "Unmet dependencies"
severity = "warn"
title = "App needs another app first"
body = "The software you're trying to install needs something else installed first — similar to a game on Android requiring Google Play Services. Let the installer fix it automatically: sudo apt --fix-broken install"
# ── Terminal basics ───────────────────────────────────────────────────────────
[[patterns]]
id = "permission-denied"
sources = ["journald"]
match_text = "Permission denied"
severity = "info"
title = "Permission denied"
body = "Linux files and folders are protected by a permission system. If a command fails with this error, you may need to run it as admin — put 'sudo' before the command: sudo <command> — and enter your password. Your password won't show as you type, that's normal."
# ── AppArmor ──────────────────────────────────────────────────────────────────
[[patterns]]
id = "apparmor-denial"
sources = ["journald"]
match_text = "apparmor=\"DENIED\""
severity = "info"
title = "App blocked by security policy"
body = "Ubuntu/Debian includes a security layer called AppArmor — like Android's app permissions system, but for the whole operating system. An app tried to do something outside its allowed permissions. Usually this resolves itself; if an app keeps failing, check: sudo aa-status"
# ── System ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Hardware driver file missing"
body = "Some hardware needs a 'firmware' file — a small program that tells Linux how to talk to a specific chip. Install the main firmware package: sudo apt install firmware-linux linux-firmware — restart after. Ubuntu usually handles this automatically; you may see this on Debian."
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "System ran out of memory — closed an app"
body = "Linux had to close a program to free up memory — similar to Android killing background apps when RAM is full. If this keeps happening, try closing programs you're not using."
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Storage error"
body = "Something went wrong reading or writing to the drive. This is a hardware-level issue. Install a diagnostic tool: sudo apt install smartmontools — then check: sudo smartctl -a /dev/sda"
# ── Audio ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "Sound system not responding"
body = "PipeWire is the audio manager — restart it: systemctl --user restart pipewire pipewire-pulse wireplumber — if that doesn't help, log out and back in."
[[patterns]]
id = "pulseaudio-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to pulseaudio"
severity = "warn"
title = "Sound system not responding"
body = "The audio system (PulseAudio) stopped working. Restart it: pulseaudio --kill && pulseaudio --start — or log out and back in."
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth blocked by software switch"
body = "Run: rfkill unblock bluetooth — in a terminal. Like turning Airplane Mode off on your phone."
[[patterns]]
id = "cups-server-error"
sources = ["journald"]
match_text = "Unable to connect to CUPS server"
severity = "info"
title = "Printer service not running"
body = "The printing service isn't running. Unlike Android where you'd use a manufacturer app, Linux uses a universal print system called CUPS. Start it: sudo systemctl start cups && sudo systemctl enable cups"
# ── Network ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "Wi-Fi connection failed"
body = "Couldn't connect to the network. Double-check the password, or try: nmcli device status — in a terminal to see your network devices."
# ── Media ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "missing-codec"
sources = ["journald"]
match_text = "GStreamer: Failed to find plugin"
severity = "info"
title = "Media format not supported"
body = "Linux doesn't include some video/audio formats by default for legal reasons — unlike Android which bundles them. Install them on Ubuntu/Mint: sudo apt install ubuntu-restricted-extras — this adds MP3, MP4, and other common formats."

View file

@ -0,0 +1,102 @@
[meta]
source_os = "android"
target_distro_family = "fedora"
# Android user on their first Fedora install.
# Assumes NO terminal experience. All explanations from first principles.
[log_paths]
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
# ── Package management ────────────────────────────────────────────────────────
[[patterns]]
id = "dnf-lock"
sources = ["journald"]
match_text = "Another app is currently holding the dnf lock"
severity = "warn"
title = "App installer is busy"
body = "Fedora's software installer (dnf) is already running — probably automatic background updates, similar to how Android apps update silently. Wait a minute. If it's stuck: open a terminal and type: sudo killall dnf — then try again."
[[patterns]]
id = "dnf-dep-conflict"
sources = ["journald"]
match_text = "conflicts with"
severity = "warn"
title = "Two apps conflict with each other"
body = "Two packages need something that can't be shared. Let Fedora try to fix it: sudo dnf distro-sync — this brings everything back into a consistent state."
# ── SELinux ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "selinux-denial"
sources = ["journald"]
match_text = "type=AVC"
severity = "info"
title = "Security system blocked an action"
body = "Fedora includes SELinux — a security layer that controls what each program is allowed to do, more detailed than Android's app permissions. This is usually a normal event, not a problem. If an app keeps failing, check what's being blocked: ausearch -m AVC -ts recent"
# ── System ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Hardware driver file missing"
body = "Some hardware needs a firmware file — a program that tells Linux how to talk to a specific chip. Install it: sudo dnf install linux-firmware — restart after. For some hardware (especially older WiFi cards), you may also need: sudo dnf install https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm"
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "System ran out of memory — closed an app"
body = "Linux had to close a program to free up RAM, like Android killing background apps. If this keeps happening, try closing some programs."
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Storage error"
body = "A hardware-level storage error. Install a diagnostic tool: sudo dnf install smartmontools — then check: sudo smartctl -a /dev/sda"
# ── Audio ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "Sound system not responding"
body = "Restart the audio system: systemctl --user restart pipewire pipewire-pulse wireplumber — or log out and back in."
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth blocked by software switch"
body = "Run: rfkill unblock bluetooth — in a terminal."
# ── Network ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "Wi-Fi connection failed"
body = "Couldn't connect to the network. Check: nmcli device status — in a terminal. If the adapter doesn't appear, check: dmesg | grep firmware — a missing driver may be the cause."
# ── Media ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "missing-codec"
sources = ["journald"]
match_text = "GStreamer: Failed to find plugin"
severity = "info"
title = "Media format not supported"
body = "Linux doesn't include some video/audio formats by default. Install them from RPM Fusion: first enable it: sudo dnf install https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm — then: sudo dnf install gstreamer1-plugins-bad-free gstreamer1-plugins-ugly"

View file

@ -0,0 +1,103 @@
[meta]
source_os = "android"
target_distro_family = "opensuse"
# Android user on their first openSUSE install.
# Assumes NO terminal experience. openSUSE's YaST GUI tool is a good entry point
# for users unfamiliar with terminal-based administration.
[log_paths]
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
# ── Package management ────────────────────────────────────────────────────────
[[patterns]]
id = "zypper-lock"
sources = ["journald"]
match_text = "System management is locked"
severity = "warn"
title = "App installer is busy"
body = "The software installer (zypper) is already running. Wait a minute. If it's stuck: open a terminal and type: sudo zypper ps — to see what's using it."
[[patterns]]
id = "zypper-dep-conflict"
sources = ["journald"]
match_text = "conflicts with"
severity = "warn"
title = "Two apps conflict with each other"
body = "Two packages need something that can't be shared. Run a full update to resolve: sudo zypper dup — then try again."
# ── AppArmor ──────────────────────────────────────────────────────────────────
[[patterns]]
id = "apparmor-denial"
sources = ["journald"]
match_text = "apparmor=\"DENIED\""
severity = "info"
title = "App blocked by security policy"
body = "openSUSE includes AppArmor — a security layer like Android's app permissions but for the whole system. An app was blocked from doing something. Check: sudo aa-status — YaST also has an AppArmor section under Security."
# ── YaST ──────────────────────────────────────────────────────────────────────
[[patterns]]
id = "yast-backend-fail"
sources = ["journald"]
match_text = "YaST got signal"
severity = "warn"
title = "Settings tool crashed"
body = "YaST is openSUSE's settings tool — like the Settings app on Android but for the whole system. It crashed. Try: sudo yast2 — in a terminal to run the text version, which is more stable."
# ── System ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Hardware driver file missing"
body = "Install the firmware package: sudo zypper install kernel-firmware — restart after."
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "System ran out of memory — closed an app"
body = "Linux had to close a program to free up RAM, like Android killing background apps. If this keeps happening, openSUSE's YaST -> System -> Partitioner can add or resize swap space."
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Storage error"
body = "A hardware-level storage error. Install: sudo zypper install smartmontools — then check: sudo smartctl -a /dev/sda"
# ── Audio ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "Sound system not responding"
body = "Restart the audio system: systemctl --user restart pipewire pipewire-pulse wireplumber — or log out and back in."
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth blocked by software switch"
body = "Run: rfkill unblock bluetooth — in a terminal. YaST -> Network -> Bluetooth also has a toggle."
# ── Network ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "Wi-Fi connection failed"
body = "Couldn't connect to the network. Check: nmcli device status — or use YaST -> Network Settings to diagnose."

View file

@ -0,0 +1,146 @@
[meta]
source_os = "linux"
target_distro_family = "debian"
# Arch/Manjaro/EndeavourOS user moving to Debian/Ubuntu/Mint.
# Body text assumes pacman, AUR, and rolling release familiarity.
[log_paths]
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
# ── apt / dpkg ────────────────────────────────────────────────────────────────
[[patterns]]
id = "apt-lock"
sources = ["journald"]
match_text = "Could not get lock /var/lib/dpkg/lock"
severity = "warn"
title = "Package manager is locked"
body = "Another apt process is running — often unattended-upgrades (automatic background updates, no Arch equivalent). Wait a minute. If stuck: sudo rm /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock && sudo dpkg --configure -a"
[[patterns]]
id = "dpkg-interrupted"
sources = ["journald"]
match_text = "dpkg was interrupted"
severity = "warn"
title = "Package install was interrupted"
body = "Like a pacman transaction that got killed, but dpkg needs manual recovery. Fix: sudo dpkg --configure -a"
[[patterns]]
id = "apt-unmet-dependency"
sources = ["journald"]
match_text = "Unmet dependencies"
severity = "warn"
title = "Package dependency conflict"
body = "Unlike pacman which puts the conflict choice on you, apt tries to auto-resolve. Let it: sudo apt --fix-broken install — if it can't, read the message for which packages conflict."
# ── System ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Firmware file missing"
body = "On Debian: sudo apt install firmware-linux firmware-linux-nonfree (enable non-free sources first). On Ubuntu: sudo apt install linux-firmware. Unlike Arch's single linux-firmware package, Debian splits firmware by license."
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "OOM killer fired"
body = "Debian stable has conservative defaults — Ubuntu enables zswap, Debian doesn't. If you used zram-generator on Arch, set up a swapfile here: fallocate -l 4G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile"
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Disk I/O error"
body = "Check SMART: sudo smartctl -a /dev/sdX — install: sudo apt install smartmontools"
[[patterns]]
id = "locale-error"
sources = ["journald"]
match_text = "failed to set locale"
severity = "info"
title = "Locale configuration error"
body = "Debian generates locales via dpkg-reconfigure: sudo dpkg-reconfigure locales — select your locale in the curses UI. Unlike Arch where you edit /etc/locale.gen directly, Debian abstracts this."
# ── AppArmor ──────────────────────────────────────────────────────────────────
[[patterns]]
id = "apparmor-denial"
sources = ["journald"]
match_text = "apparmor=\"DENIED\""
severity = "info"
title = "AppArmor access denied"
body = "Debian/Ubuntu ships AppArmor — Arch doesn't use MAC by default. An app is blocked by a security profile. Check: sudo aa-status — audit: sudo aa-logprof — or put the profile in complain mode: sudo aa-complain /etc/apparmor.d/<profile>"
# ── Audio ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "PipeWire not responding"
body = "Ubuntu 22.04+/Debian 12+ ship PipeWire like Arch. Restart: systemctl --user restart pipewire pipewire-pulse wireplumber"
[[patterns]]
id = "pulseaudio-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to pulseaudio"
severity = "warn"
title = "PulseAudio not responding"
body = "Older Debian systems still use PulseAudio. Restart: pulseaudio --kill && pulseaudio --start — you can migrate to PipeWire: sudo apt install pipewire-audio"
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth rfkill blocked"
body = "rfkill unblock bluetooth — same as Arch."
# ── GPU / display ─────────────────────────────────────────────────────────────
[[patterns]]
id = "gpu-hang"
sources = ["kmsg"]
match_text = "GPU HANG"
severity = "warn"
title = "GPU hang"
body = "GPU stopped responding. For NVIDIA on Ubuntu: sudo apt install nvidia-driver-<version> or ubuntu-drivers autoinstall. Debian requires non-free sources: apt install nvidia-driver"
# ── Network ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "NetworkManager: connection failed"
body = "nmcli device status — Debian minimal installs may use ifupdown instead of NetworkManager. Check: systemctl status NetworkManager — install if missing: sudo apt install network-manager"
# ── Printing ──────────────────────────────────────────────────────────────────
[[patterns]]
id = "cups-server-error"
sources = ["journald"]
match_text = "Unable to connect to CUPS server"
severity = "info"
title = "Printer service not running"
body = "sudo systemctl start cups && sudo systemctl enable cups — Debian/Ubuntu handle printing through CUPS; Arch also uses CUPS but it's not always enabled by default."
# ── Gaming ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "proton-runtime-missing"
sources = ["applog:proton"]
match_text = "wine: cannot find"
severity = "warn"
title = "Proton runtime issue"
body = "Right-click game in Steam -> Properties -> Local Files -> Verify integrity."

View file

@ -0,0 +1,136 @@
[meta]
source_os = "linux"
target_distro_family = "fedora"
# Arch/Manjaro/EndeavourOS user moving to Fedora.
# Body text assumes pacman, AUR, and rolling release familiarity.
[log_paths]
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
# ── DNF / RPM ────────────────────────────────────────────────────────────────
[[patterns]]
id = "dnf-lock"
sources = ["journald"]
match_text = "Another app is currently holding the dnf lock"
severity = "warn"
title = "DNF package manager is locked"
body = "dnf-automatic (Fedora's equivalent of Arch's unattended auto-updates, though Arch doesn't do auto-updates) is probably running. Wait it out or: sudo ps aux | grep dnf"
[[patterns]]
id = "dnf-dep-conflict"
sources = ["journald"]
match_text = "conflicts with"
severity = "warn"
title = "Package dependency conflict"
body = "Unlike pacman where you resolve conflicts manually, dnf tries to auto-resolve. It usually succeeds. If not: sudo dnf distro-sync — the Fedora equivalent of pacman -Syu for bringing the system fully in sync."
[[patterns]]
id = "dnf-gpg-key"
sources = ["journald"]
match_text = "GPG key retrieval failed"
severity = "warn"
title = "Repository GPG key missing"
body = "Import the key: sudo rpm --import /path/to/key.gpg — or re-run with: sudo dnf install --nogpgcheck (only if you trust the source). RPM Fusion keys are imported automatically when you enable the repo."
# ── SELinux (new concept for Arch users) ─────────────────────────────────────
[[patterns]]
id = "selinux-denial"
sources = ["journald"]
match_text = "type=AVC"
severity = "info"
title = "SELinux access denied"
body = "Fedora ships SELinux enforcing by default — there's nothing like this on Arch by default. A security policy is blocking an action. Check what's blocked: ausearch -m AVC -ts recent — get a fix suggestion: sealert -a /var/log/audit/audit.log — don't just set SELinux to permissive; that defeats the security model."
[[patterns]]
id = "selinux-context-wrong"
sources = ["journald"]
match_text = "restorecon"
severity = "info"
title = "SELinux file context mismatch"
body = "A file has the wrong security label — common when copying files from an Arch system or an external drive. Fix: sudo restorecon -Rv /path/to/file"
# ── System ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Firmware file missing"
body = "sudo dnf install linux-firmware — same coverage as Arch's linux-firmware package. Some chips may need RPM Fusion nonfree: sudo dnf install rpmfusion-nonfree-release-$(rpm -E %fedora)"
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "OOM killer fired"
body = "A process was killed for RAM. Fedora enables zswap by default on modern releases. If you used zram on Arch, install zram-generator: sudo dnf install zram-generator"
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Disk I/O error"
body = "Check SMART: sudo smartctl -a /dev/sdX — install: sudo dnf install smartmontools"
# ── Audio ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "PipeWire not responding"
body = "Both Arch and Fedora ship PipeWire. Restart: systemctl --user restart pipewire pipewire-pulse wireplumber"
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth rfkill blocked"
body = "rfkill unblock bluetooth — same as Arch."
# ── GPU / display ─────────────────────────────────────────────────────────────
[[patterns]]
id = "gpu-hang"
sources = ["kmsg"]
match_text = "GPU HANG"
severity = "warn"
title = "GPU hang"
body = "GPU stopped responding. For NVIDIA on Fedora, RPM Fusion is the right source: sudo dnf install akmod-nvidia — unlike Arch's nvidia-dkms from the official repos."
[[patterns]]
id = "xwayland-crash"
sources = ["journald"]
match_text = "XWayland server terminated unexpectedly"
severity = "warn"
title = "XWayland crashed"
body = "X11 apps dead until session restart. Fedora GNOME defaults to Wayland like Arch KDE/GNOME can."
# ── Network ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "NetworkManager: connection failed"
body = "nmcli device status — same NetworkManager as Arch. Firmware issues: sudo dmesg | grep firmware"
# ── Gaming ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "proton-runtime-missing"
sources = ["applog:proton"]
match_text = "wine: cannot find"
severity = "warn"
title = "Proton runtime issue"
body = "Right-click game in Steam -> Properties -> Local Files -> Verify integrity. Steam on Fedora: sudo dnf install steam (from RPM Fusion free)."

View file

@ -0,0 +1,130 @@
[meta]
source_os = "linux"
target_distro_family = "opensuse"
# Arch/Manjaro/EndeavourOS user moving to openSUSE Tumbleweed or Leap.
# Body text assumes pacman, AUR, and rolling release familiarity.
[log_paths]
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
# ── zypper / RPM ─────────────────────────────────────────────────────────────
[[patterns]]
id = "zypper-lock"
sources = ["journald"]
match_text = "System management is locked"
severity = "warn"
title = "zypper package manager is locked"
body = "Another zypper or PackageKit (GNOME Software) process is running. Wait it out or check: sudo ps aux | grep zypper"
[[patterns]]
id = "zypper-dep-conflict"
sources = ["journald"]
match_text = "conflicts with"
severity = "warn"
title = "Package dependency conflict"
body = "Unlike pacman which puts the conflict on you immediately, zypper presents resolution options. zypper dup (distribution upgrade) is more aggressive than zypper up and usually resolves what zypper up won't."
[[patterns]]
id = "zypper-gpg-key"
sources = ["journald"]
match_text = "does not verify"
severity = "warn"
title = "Repository signature not trusted"
body = "Auto-import keys: sudo zypper --gpg-auto-import-keys ref — unlike pacman-key, zypper manages repo keys through RPM's keyring."
# ── AppArmor ──────────────────────────────────────────────────────────────────
[[patterns]]
id = "apparmor-denial"
sources = ["journald"]
match_text = "apparmor=\"DENIED\""
severity = "info"
title = "AppArmor access denied"
body = "openSUSE ships AppArmor — Arch doesn't use MAC by default. An app is blocked by a profile. Check: sudo aa-status — audit and fix: sudo aa-logprof — or set the profile to complain mode: sudo aa-complain <profile-name>"
# ── YaST ──────────────────────────────────────────────────────────────────────
[[patterns]]
id = "yast-backend-fail"
sources = ["journald"]
match_text = "YaST got signal"
severity = "warn"
title = "YaST configuration tool crashed"
body = "YaST is openSUSE's graphical admin tool — no Arch equivalent. If it crashed mid-operation: sudo yast2 in a terminal to run the text-mode version, which is more stable for recovery."
# ── System ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Firmware file missing"
body = "sudo zypper install kernel-firmware — similar to Arch's linux-firmware in scope."
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "OOM killer fired"
body = "A process was killed for RAM. If you used zram-generator on Arch, openSUSE supports it too: sudo zypper install zram-generator — or set up swap via YaST -> System -> Partitioner."
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Disk I/O error"
body = "Check SMART: sudo smartctl -a /dev/sdX — install: sudo zypper install smartmontools"
# ── Audio ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "PipeWire not responding"
body = "Tumbleweed ships PipeWire like Arch. Restart: systemctl --user restart pipewire pipewire-pulse wireplumber"
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth rfkill blocked"
body = "rfkill unblock bluetooth — same as Arch."
# ── GPU / display ─────────────────────────────────────────────────────────────
[[patterns]]
id = "gpu-hang"
sources = ["kmsg"]
match_text = "GPU HANG"
severity = "warn"
title = "GPU hang"
body = "GPU stopped responding. For NVIDIA on openSUSE: use the NVIDIA OBS repo (similar to Arch's nvidia-dkms from official repos). AMD users: mesa is in the default repos."
# ── Network ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "NetworkManager: connection failed"
body = "nmcli device status — openSUSE may use Wicked on server installs instead of NetworkManager. Check: systemctl status NetworkManager wicked"
# ── Gaming ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "proton-runtime-missing"
sources = ["applog:proton"]
match_text = "wine: cannot find"
severity = "warn"
title = "Proton runtime issue"
body = "Right-click game in Steam -> Properties -> Local Files -> Verify integrity. Steam on openSUSE: sudo zypper install steam (from the games repo on OBS)."

View file

@ -0,0 +1,186 @@
[meta]
source_os = "linux"
target_distro_family = "arch"
# Debian/Ubuntu/Mint user on their first Arch install.
# Body text assumes comfort with apt and systemd but not AUR or rolling release.
[log_paths]
pacman = "/var/log/pacman.log"
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
lutris = "~/.cache/lutris/logs/lutris.log"
# ── pacman / AUR ─────────────────────────────────────────────────────────────
[[patterns]]
id = "pacman-db-lock"
sources = ["journald", "applog:pacman"]
match_text = "could not lock database: File exists"
severity = "warn"
title = "pacman database locked"
body = "Lock file left from a crashed pacman run — like dpkg getting killed mid-install. If nothing is running: sudo rm /var/lib/pacman/db.lck — verify first: fuser /var/lib/pacman/db.lck"
[[patterns]]
id = "pacman-dep-conflict"
sources = ["journald", "applog:pacman"]
match_text = "conflicting dependencies"
severity = "warn"
title = "Dependency conflict"
body = "Unlike apt, pacman won't auto-resolve conflicts — you decide. Usually one package replaces another (e.g. pipewire-pulse replaces pulseaudio). Remove the conflicting package first, then retry."
[[patterns]]
id = "pacman-conflicting-files"
sources = ["journald", "applog:pacman"]
match_text = "error: failed to commit transaction (conflicting files)"
severity = "warn"
title = "Conflicting files on install"
body = "A file already exists on disk that the package wants to own. Check which package owns it: pacman -Qo /path/to/file — then remove the stale file or use --overwrite if you're sure it's safe."
[[patterns]]
id = "partial-upgrade-warning"
sources = ["applog:pacman"]
match_text = "warning: database file for"
severity = "info"
title = "Package database out of sync"
body = "On Arch, pacman -Sy (sync without upgrade) is dangerous — partial upgrades break the system. Unlike apt where partial syncs are fine, on Arch always use pacman -Syu. This is the biggest rule to learn coming from Debian."
[[patterns]]
id = "aur-build-failure"
sources = ["journald", "applog:pacman"]
match_text = "error: failed to build"
severity = "warn"
title = "AUR package build failed"
body = "The AUR is source-based — no binary packages, makepkg compiles from a PKGBUILD. Read the full output for the cause: missing makedepend, broken upstream URL, or bad patch. Check the package's AUR comments page for known fixes."
[[patterns]]
id = "aur-pgp-key"
sources = ["journald", "applog:pacman"]
match_text = "unknown public key"
severity = "warn"
title = "PGP key not in keyring"
body = "AUR packages verify signatures — different from apt's keyring model. Import the key: gpg --recv-keys <keyid> — or if the PKGBUILD lists validpgpkeys, import exactly those."
[[patterns]]
id = "makepkg-missing-deps"
sources = ["journald", "applog:pacman"]
match_text = "Missing dependencies"
severity = "warn"
title = "AUR build dependencies missing"
body = "AUR helpers like paru/yay resolve makedepends automatically. If building manually: sudo pacman -S <dep> first. This is like build-dep in apt but you have to do it manually with plain makepkg."
[[patterns]]
id = "chaotic-aur-sig-fail"
sources = ["journald", "applog:pacman"]
match_text = "signature from"
severity = "warn"
title = "Package signature verification failed"
body = "Chaotic-AUR key not trusted. Import it: sudo pacman-key --recv-key 3056513887B78AEB --keyserver keyserver.ubuntu.com && sudo pacman-key --lsign-key 3056513887B78AEB — then retry."
# ── Kernel / DKMS ─────────────────────────────────────────────────────────────
[[patterns]]
id = "dkms-build-fail"
sources = ["journald"]
match_text = "Error! Build of"
severity = "warn"
title = "DKMS module failed to build"
body = "A kernel module didn't compile after a kernel update. On Arch's rolling release this happens more than on Debian stable. Check: dkms status — then reinstall the dkms package or wait for an AUR update. Debian had linux-headers; here it's linux-headers (or linux-cachyos-headers on CachyOS)."
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Firmware file missing"
body = "sudo pacman -S linux-firmware — similar to firmware-linux-nonfree on Debian but one package covers most hardware. Specific chips (Realtek wifi) may need AUR packages."
# ── System ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "locale-not-set"
sources = ["journald"]
match_text = "Cannot set LC_ALL to default locale"
severity = "info"
title = "Locale not generated"
body = "Unlike Debian, Arch doesn't pre-generate locales. Edit /etc/locale.gen (uncomment your locale, e.g. en_US.UTF-8), then: sudo locale-gen — and set LANG=en_US.UTF-8 in /etc/locale.conf."
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "OOM killer fired"
body = "A process was killed for RAM. Arch doesn't enable zswap by default like Ubuntu does. Add zram: sudo pacman -S zram-generator — or add a swapfile."
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Disk I/O error"
body = "Storage error on a block device. Check SMART: sudo smartctl -a /dev/sdX — or for NVMe: sudo nvme smart-log /dev/nvme0."
# ── Audio ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "PipeWire not responding"
body = "systemctl --user restart pipewire pipewire-pulse wireplumber — Arch defaults to PipeWire; no PulseAudio fallback like Ubuntu has."
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth rfkill blocked"
body = "rfkill unblock bluetooth — if hard-blocked, check BIOS or a physical switch."
# ── GPU / display ─────────────────────────────────────────────────────────────
[[patterns]]
id = "gpu-hang"
sources = ["kmsg"]
match_text = "GPU HANG"
severity = "warn"
title = "GPU hang"
body = "GPU stopped responding. On Arch with AMD: check mesa version vs kernel version — both roll together. On NVIDIA: check nvidia-dkms matches the running kernel."
[[patterns]]
id = "xwayland-crash"
sources = ["journald"]
match_text = "XWayland server terminated unexpectedly"
severity = "warn"
title = "XWayland crashed"
body = "X11 apps will be dead until session restart. If reproducible with a specific app, check for a Wayland-native version or force X11 with WAYLAND_DISPLAY= unset."
# ── Network ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "NetworkManager: connection failed"
body = "nmcli device status — if a wifi adapter is missing, check dmesg for firmware errors. Arch ships most firmware in linux-firmware; some Realtek chips need AUR packages."
# ── Gaming ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "proton-runtime-missing"
sources = ["applog:proton"]
match_text = "wine: cannot find"
severity = "warn"
title = "Proton runtime issue"
body = "Right-click game in Steam -> Properties -> Local Files -> Verify integrity."
[[patterns]]
id = "lutris-wine-fail"
sources = ["applog:lutris"]
match_text = "Wine is not installed"
severity = "warn"
title = "Lutris: Wine not found"
body = "Lutris needs a Wine runner. In Lutris: Preferences -> Runners -> Wine -> Install — or: paru -S wine-staging"

View file

@ -0,0 +1,144 @@
[meta]
source_os = "linux"
target_distro_family = "fedora"
# Debian/Ubuntu/Mint user on their first Fedora/RHEL/CentOS install.
# Body text assumes apt/dpkg familiarity, no RPM experience.
[log_paths]
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
# ── DNF / RPM ────────────────────────────────────────────────────────────────
[[patterns]]
id = "dnf-lock"
sources = ["journald"]
match_text = "Another app is currently holding the dnf lock"
severity = "warn"
title = "DNF package manager is locked"
body = "Another dnf process is running — like apt being held by unattended-upgrades. Wait for it to finish or check: sudo ps aux | grep dnf — then kill if stuck."
[[patterns]]
id = "rpm-db-corrupt"
sources = ["journald"]
match_text = "rpmdb"
severity = "warn"
title = "RPM database issue"
body = "Like a corrupted dpkg database. Rebuild it: sudo rpm --rebuilddb — then retry your dnf command."
[[patterns]]
id = "dnf-dep-conflict"
sources = ["journald"]
match_text = "conflicts with"
severity = "warn"
title = "Package dependency conflict"
body = "DNF can auto-resolve some conflicts but not all. Read the conflict message — usually one package provides what another needs. Try: sudo dnf distro-sync to bring everything in sync."
[[patterns]]
id = "dnf-gpg-key"
sources = ["journald"]
match_text = "GPG key retrieval failed"
severity = "warn"
title = "Repository GPG key missing"
body = "A repo's signing key isn't trusted. Import it: sudo rpm --import /path/to/key.gpg — or re-enable the repo's key: sudo dnf repoinfo <repo>"
# ── SELinux ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "selinux-denial"
sources = ["journald"]
match_text = "type=AVC"
severity = "info"
title = "SELinux access denied"
body = "Fedora ships SELinux enforcing by default — there's nothing like this on Debian/Ubuntu. This is a security policy denial, not a bug. Check: ausearch -m AVC -ts recent — then run: sealert -a /var/log/audit/audit.log for a fix suggestion."
[[patterns]]
id = "selinux-context-wrong"
sources = ["journald"]
match_text = "restorecon"
severity = "info"
title = "SELinux file context mismatch"
body = "A file has the wrong security label. Fix: sudo restorecon -Rv /path/to/file — common after copying files from Debian or moving data between filesystems."
# ── System ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Firmware file missing"
body = "On Fedora: sudo dnf install linux-firmware — or for specific hardware check RPM Fusion's nonfree repo. Enable it: sudo dnf install https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm"
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "OOM killer fired"
body = "A process was killed for RAM. Fedora enables zswap by default on modern releases, but adding a swapfile helps on low-RAM systems: sudo systemctl enable --now systemd-swap"
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Disk I/O error"
body = "Storage error. Check SMART: sudo smartctl -a /dev/sdX — smartmontools is in the default Fedora repos."
# ── Audio ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "PipeWire not responding"
body = "Fedora pioneered PipeWire adoption — it's the default. Restart: systemctl --user restart pipewire pipewire-pulse wireplumber"
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth rfkill blocked"
body = "rfkill unblock bluetooth — if hard-blocked, check BIOS or a physical switch."
# ── GPU / display ─────────────────────────────────────────────────────────────
[[patterns]]
id = "gpu-hang"
sources = ["kmsg"]
match_text = "GPU HANG"
severity = "warn"
title = "GPU hang"
body = "GPU stopped responding. Check: dnf list installed | grep -i nvidia (or mesa) — RPM Fusion is the right source for proprietary NVIDIA drivers on Fedora."
[[patterns]]
id = "xwayland-crash"
sources = ["journald"]
match_text = "XWayland server terminated unexpectedly"
severity = "warn"
title = "XWayland crashed"
body = "Fedora ships GNOME on Wayland by default; XWayland handles X11 apps. Restart your session to recover X11 apps."
# ── Network ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "NetworkManager: connection failed"
body = "nmcli device status — Fedora ships NetworkManager by default, same as Ubuntu. If a wifi adapter is missing, check: dmesg | grep firmware"
# ── Gaming ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "proton-runtime-missing"
sources = ["applog:proton"]
match_text = "wine: cannot find"
severity = "warn"
title = "Proton runtime issue"
body = "Right-click game in Steam -> Properties -> Local Files -> Verify integrity. Steam on Fedora may also need: sudo dnf install steam (from RPM Fusion free repo)."

View file

@ -0,0 +1,120 @@
[meta]
source_os = "linux"
target_distro_family = "opensuse"
# Debian/Ubuntu/Mint user on their first openSUSE Tumbleweed or Leap install.
# Body text assumes apt/dpkg familiarity; explains zypper and YaST concepts.
[log_paths]
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
# ── zypper / RPM ─────────────────────────────────────────────────────────────
[[patterns]]
id = "zypper-lock"
sources = ["journald"]
match_text = "System management is locked"
severity = "warn"
title = "zypper package manager is locked"
body = "Another zypper or PackageKit process is running — like apt being held by unattended-upgrades. Wait it out or check: sudo ps aux | grep zypper — the lock file is at /var/run/zypp.pid"
[[patterns]]
id = "zypper-dep-conflict"
sources = ["journald"]
match_text = "conflicts with"
severity = "warn"
title = "Package dependency conflict"
body = "zypper presents conflict resolution choices interactively. If running non-interactively, read the error — usually one package needs to be removed or a different provider selected. zypper dup (distribution upgrade) resolves more aggressively than zypper up."
[[patterns]]
id = "zypper-gpg-key"
sources = ["journald"]
match_text = "does not verify"
severity = "warn"
title = "Repository signature not trusted"
body = "A repo key isn't trusted. Accept it: sudo zypper --gpg-auto-import-keys ref — or import manually: sudo rpm --import /path/to/key.gpg"
# ── AppArmor ──────────────────────────────────────────────────────────────────
[[patterns]]
id = "apparmor-denial"
sources = ["journald"]
match_text = "apparmor=\"DENIED\""
severity = "info"
title = "AppArmor access denied"
body = "openSUSE ships AppArmor (similar to Ubuntu, not Debian default). An app is blocked by its security profile. Check: sudo aa-status — then audit the profile with: sudo aa-logprof"
# ── System ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Firmware file missing"
body = "sudo zypper install kernel-firmware — openSUSE packages firmware separately like Debian but the package is called kernel-firmware, not firmware-linux."
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "OOM killer fired"
body = "A process was killed for RAM. openSUSE sets up swap during install; if you skipped it, add a swapfile via YaST -> System -> Partitioner or manually with dd + mkswap."
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Disk I/O error"
body = "Storage error. Check SMART: sudo smartctl -a /dev/sdX — install smartmontools first: sudo zypper install smartmontools"
# ── Audio ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "PipeWire not responding"
body = "Tumbleweed ships PipeWire by default. Restart: systemctl --user restart pipewire pipewire-pulse wireplumber"
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth rfkill blocked"
body = "rfkill unblock bluetooth — if hard-blocked, check BIOS or a physical switch."
# ── GPU / display ─────────────────────────────────────────────────────────────
[[patterns]]
id = "gpu-hang"
sources = ["kmsg"]
match_text = "GPU HANG"
severity = "warn"
title = "GPU hang"
body = "GPU stopped responding. For NVIDIA on openSUSE, use the official NVIDIA repo: https://www.nvidia.com/object/unix.html — or the community packages.opensuse.org repo."
# ── Network ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "NetworkManager: connection failed"
body = "nmcli device status — openSUSE uses NetworkManager by default. For wifi firmware issues: sudo zypper install kernel-firmware-iwlwifi (Intel) or kernel-firmware-realtek (Realtek)."
# ── Gaming ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "proton-runtime-missing"
sources = ["applog:proton"]
match_text = "wine: cannot find"
severity = "warn"
title = "Proton runtime issue"
body = "Right-click game in Steam -> Properties -> Local Files -> Verify integrity. Steam on openSUSE: sudo zypper install steam (from the games repo on OBS)."

View file

@ -0,0 +1,65 @@
[meta]
source_os = "supplement"
target_distro_family = "any"
# Supplementary patterns for users dual-booting macOS alongside any Linux distro.
# These patterns cover coexistence-specific issues unique to Apple hardware.
# This file is merged on top of the primary migration pattern file.
[log_paths]
# ── Apple T2 / Secure Boot ────────────────────────────────────────────────────
[[patterns]]
id = "t2-secure-boot"
sources = ["kmsg", "journald"]
match_text = "Secure Boot"
severity = "warn"
title = "Apple T2 Secure Boot blocking Linux"
body = "Intel Macs with a T2 chip require Secure Boot to be disabled before Linux can boot. Boot into macOS Recovery (hold Cmd+R at startup) -> Utilities -> Startup Security Utility -> set Secure Boot to 'No Security' and allow booting from external media. Apple Silicon (M1/M2) Macs cannot dual-boot Linux at all — see Asahi Linux for the current state."
[[patterns]]
id = "apple-wifi-firmware"
sources = ["kmsg"]
match_text = "brcmfmac: brcmf_fw_alloc_request"
severity = "warn"
title = "Apple WiFi firmware not loading"
body = "Broadcom WiFi chips in Macs need proprietary firmware. Extract it from the macOS partition: mount your macOS partition and copy from /Volumes/Macintosh HD/usr/share/firmware/wifi/ — or install apple-firmware-wifi (check your distro's AUR or repos)."
# ── HFS+ / APFS ───────────────────────────────────────────────────────────────
[[patterns]]
id = "apfs-not-mounted"
sources = ["journald"]
match_text = "apfs: module not found"
severity = "info"
title = "macOS APFS partition not readable"
body = "Linux can't read APFS (macOS's filesystem) natively. To access files: sudo apt install apfs-fuse (Debian) or paru -S apfs-fuse-git (Arch). Mount: apfs-fuse /dev/sdXN /mnt/mac — read-only access only."
[[patterns]]
id = "hfsplus-not-mounted"
sources = ["journald"]
match_text = "hfsplus: Journal not clean"
severity = "warn"
title = "HFS+ partition not cleanly unmounted"
body = "The macOS HFS+ partition (older Macs) wasn't unmounted cleanly. Mount in macOS and run Disk Utility -> First Aid to fix it. Or force Linux mount: sudo mount -o force /dev/sdXN /mnt/mac"
# ── rEFInd / boot manager ────────────────────────────────────────────────────
[[patterns]]
id = "refind-missing"
sources = ["journald"]
match_text = "Boot0001"
severity = "info"
title = "EFI boot entry may be missing"
body = "macOS may have reset the EFI boot order after an update, removing the Linux entry. rEFInd is the recommended boot manager for Mac dual-boot: it auto-detects both macOS and Linux. Install: sudo refind-install — or reinstall GRUB EFI and re-add it with efibootmgr."
# ── Clock skew ────────────────────────────────────────────────────────────────
[[patterns]]
id = "rtc-time-wrong"
sources = ["journald"]
match_text = "RTC time"
severity = "info"
title = "System clock drifted after macOS boot"
body = "macOS stores the hardware clock in local time; Linux stores it in UTC. This causes clock drift in dual-boot. Fix in Linux: timedatectl set-local-rtc 0 — then set macOS to UTC by running in Terminal: sudo systemsetup -setusingnetworktime off && sudo systemsetup -settime $(date -u +%H:%M:%S)"

View file

@ -0,0 +1,75 @@
[meta]
source_os = "supplement"
target_distro_family = "any"
# Supplementary patterns for users dual-booting Windows alongside any Linux distro.
# These patterns cover coexistence-specific issues that only appear because both OSes
# share the same hardware. This file is merged on top of the primary migration pattern file.
[log_paths]
# ── NTFS / Fast Startup ───────────────────────────────────────────────────────
[[patterns]]
id = "ntfs-volume-dirty"
sources = ["kmsg"]
match_text = "volume is dirty"
severity = "warn"
title = "Windows drive needs a check (Fast Startup)"
body = "Windows didn't shut down cleanly — it probably used Fast Startup (hibernation). Linux mounted the NTFS partition read-only to protect your data. Fix in Windows: Start -> Power -> hold Shift and click Shut Down (real shutdown, not hibernate). Then turn Fast Startup off: Control Panel -> Power Options -> 'Choose what the power buttons do' -> uncheck 'Turn on fast startup'."
[[patterns]]
id = "ntfs-force-required"
sources = ["kmsg"]
match_text = "Dirty flag is set"
severity = "warn"
title = "NTFS drive mounted read-only (dirty flag)"
body = "Windows left the NTFS filesystem marked dirty. Boot into Windows and do a full shutdown (Shift+Shut Down), or force-fix on Linux: sudo ntfsfix /dev/sdXN — replace sdXN with the partition shown in the error."
[[patterns]]
id = "ntfs-hibernation"
sources = ["kmsg"]
match_text = "Windows is hibernated"
severity = "warn"
title = "Windows is hibernated — partition locked"
body = "Linux found a Windows hibernation file (hiberfil.sys) and can't write to the NTFS partition safely. You must resume and properly shut down Windows first. To remove the hibernation file permanently: in Windows as admin, run: powercfg /h off"
# ── Clock skew ────────────────────────────────────────────────────────────────
[[patterns]]
id = "rtc-time-wrong"
sources = ["journald"]
match_text = "RTC time"
severity = "info"
title = "System clock drifted after Windows boot"
body = "Windows stores the hardware clock in local time; Linux stores it in UTC. Dual-booting causes clock drift between sessions. Fix permanently in Linux: timedatectl set-local-rtc 0 (keep Linux correct and fix Windows instead). Or in Windows, add a registry key to use UTC: HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\TimeZoneInformation, add DWORD RealTimeIsUniversal = 1."
# ── GRUB overwritten by Windows ───────────────────────────────────────────────
[[patterns]]
id = "grub-missing-windows-update"
sources = ["journald"]
match_text = "error: no such device"
severity = "warn"
title = "GRUB may have been overwritten"
body = "Windows Update sometimes overwrites the EFI boot entry. If Linux stopped booting after a Windows update: boot from your Linux USB installer -> rescue/chroot -> reinstall GRUB: grub-install /dev/sdX && update-grub (Debian/Ubuntu) or grub-install /dev/sdX && grub-mkconfig -o /boot/grub/grub.cfg (Arch)."
# ── BitLocker ─────────────────────────────────────────────────────────────────
[[patterns]]
id = "bitlocker-blocked"
sources = ["kmsg"]
match_text = "BitLocker"
severity = "info"
title = "BitLocker encrypted partition"
body = "This Windows partition is BitLocker-encrypted. Linux can mount it with dislocker: sudo apt install dislocker (Debian) or paru -S dislocker (Arch). You'll need the BitLocker recovery key from your Microsoft account."
# ── Shared NTFS partition permissions ────────────────────────────────────────
[[patterns]]
id = "ntfs-permission-error"
sources = ["journald"]
match_text = "ntfs-3g: Failed to open"
severity = "warn"
title = "NTFS permission error"
body = "ntfs-3g can't open the Windows partition. Check your /etc/fstab mount options — add uid=1000,gid=1000,umask=022 to give your Linux user access. Make sure Windows is fully shut down first."

View file

@ -0,0 +1,172 @@
[meta]
source_os = "linux"
target_distro_family = "arch"
# Fedora/RHEL/CentOS user on their first Arch install.
# Body text assumes DNF, SELinux, and RPM Fusion familiarity.
[log_paths]
pacman = "/var/log/pacman.log"
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
lutris = "~/.cache/lutris/logs/lutris.log"
# ── pacman / AUR ─────────────────────────────────────────────────────────────
[[patterns]]
id = "pacman-db-lock"
sources = ["journald", "applog:pacman"]
match_text = "could not lock database: File exists"
severity = "warn"
title = "pacman database locked"
body = "Lock file left from a crashed pacman run — like a dnf transaction that got killed. Remove if nothing is running: sudo rm /var/lib/pacman/db.lck — check first: fuser /var/lib/pacman/db.lck"
[[patterns]]
id = "partial-upgrade-warning"
sources = ["applog:pacman"]
match_text = "warning: database file for"
severity = "info"
title = "Package database out of sync"
body = "Arch rule #1 coming from Fedora: never run pacman -Sy (sync only). On Fedora, dnf check-update is safe; on Arch, syncing the database without upgrading breaks the system. Always: pacman -Syu"
[[patterns]]
id = "pacman-dep-conflict"
sources = ["journald", "applog:pacman"]
match_text = "conflicting dependencies"
severity = "warn"
title = "Dependency conflict"
body = "Unlike dnf which auto-resolves most conflicts, pacman puts the choice on you. Read the conflict — typically one package is being replaced (e.g. pipewire-pulse replaces pulseaudio). Remove the old one first."
[[patterns]]
id = "aur-build-failure"
sources = ["journald", "applog:pacman"]
match_text = "error: failed to build"
severity = "warn"
title = "AUR package build failed"
body = "The AUR is source-only — no binary RPM equivalent. makepkg compiles from a PKGBUILD. Read the build output; check the package's AUR comments page. paru/yay handle makedepends automatically."
[[patterns]]
id = "aur-pgp-key"
sources = ["journald", "applog:pacman"]
match_text = "unknown public key"
severity = "warn"
title = "PGP key not in keyring"
body = "Import the key: gpg --recv-keys <keyid> — different from RPM's --import; this is GnuPG's own keyring used by makepkg."
[[patterns]]
id = "chaotic-aur-sig-fail"
sources = ["journald", "applog:pacman"]
match_text = "signature from"
severity = "warn"
title = "Package signature verification failed"
body = "Chaotic-AUR key not trusted. Import: sudo pacman-key --recv-key 3056513887B78AEB --keyserver keyserver.ubuntu.com && sudo pacman-key --lsign-key 3056513887B78AEB"
# ── SELinux → no SELinux ──────────────────────────────────────────────────────
[[patterns]]
id = "selinux-remnant"
sources = ["journald"]
match_text = "type=AVC"
severity = "info"
title = "SELinux audit entry (ignored on Arch)"
body = "Arch doesn't ship SELinux by default — if you see this, you may have carried over a journal from a Fedora partition, or installed selinux-utils manually. No action needed unless you intentionally set up SELinux on Arch."
# ── Kernel / DKMS ─────────────────────────────────────────────────────────────
[[patterns]]
id = "dkms-build-fail"
sources = ["journald"]
match_text = "Error! Build of"
severity = "warn"
title = "DKMS module failed to build"
body = "A kernel module didn't compile after a kernel update. More frequent on Arch's rolling kernel than on Fedora's slower cadence. Check: dkms status — reinstall the failing module package."
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Firmware file missing"
body = "sudo pacman -S linux-firmware — covers most hardware. Some chips need AUR packages (similar to RPM Fusion nonfree on Fedora)."
# ── System ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "locale-not-set"
sources = ["journald"]
match_text = "Cannot set LC_ALL to default locale"
severity = "info"
title = "Locale not generated"
body = "Unlike Fedora where locale is configured in the installer, Arch requires manual setup. Edit /etc/locale.gen, uncomment your locale, run: sudo locale-gen — then set LANG in /etc/locale.conf."
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "OOM killer fired"
body = "A process was killed for RAM. Fedora enables zswap by default; Arch doesn't. Add zram: sudo pacman -S zram-generator"
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Disk I/O error"
body = "Check SMART: sudo smartctl -a /dev/sdX — install smartmontools: sudo pacman -S smartmontools"
# ── Audio ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "PipeWire not responding"
body = "Both Fedora and Arch ship PipeWire by default. Restart: systemctl --user restart pipewire pipewire-pulse wireplumber"
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth rfkill blocked"
body = "rfkill unblock bluetooth — check: rfkill list to distinguish hard vs soft block."
# ── GPU / display ─────────────────────────────────────────────────────────────
[[patterns]]
id = "gpu-hang"
sources = ["kmsg"]
match_text = "GPU HANG"
severity = "warn"
title = "GPU hang"
body = "GPU stopped responding. On Arch, AMD uses mesa (pacman -S mesa); NVIDIA uses nvidia-dkms. Unlike Fedora where RPM Fusion handles NVIDIA, on Arch install from the official repos."
[[patterns]]
id = "xwayland-crash"
sources = ["journald"]
match_text = "XWayland server terminated unexpectedly"
severity = "warn"
title = "XWayland crashed"
body = "X11 apps dead until session restart. Fedora GNOME also defaults to Wayland; the behavior is the same."
# ── Network ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "NetworkManager: connection failed"
body = "nmcli device status — same NetworkManager as Fedora. If a wifi adapter is missing, check dmesg for firmware errors."
# ── Gaming ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "proton-runtime-missing"
sources = ["applog:proton"]
match_text = "wine: cannot find"
severity = "warn"
title = "Proton runtime issue"
body = "Right-click game in Steam -> Properties -> Local Files -> Verify integrity."

View file

@ -0,0 +1,138 @@
[meta]
source_os = "linux"
target_distro_family = "debian"
# Fedora/RHEL user moving to Debian/Ubuntu/Mint.
# Body text assumes DNF, SELinux, and systemd familiarity.
[log_paths]
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
# ── apt / dpkg ────────────────────────────────────────────────────────────────
[[patterns]]
id = "apt-lock"
sources = ["journald"]
match_text = "Could not get lock /var/lib/dpkg/lock"
severity = "warn"
title = "Package manager is locked"
body = "Another apt process is running — often unattended-upgrades in the background. Wait a minute. If stuck: sudo rm /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock — then: sudo dpkg --configure -a"
[[patterns]]
id = "dpkg-interrupted"
sources = ["journald"]
match_text = "dpkg was interrupted"
severity = "warn"
title = "Package install was interrupted"
body = "A previous install didn't finish cleanly. Fix: sudo dpkg --configure -a — like an interrupted dnf transaction, but requires manual recovery."
[[patterns]]
id = "apt-unmet-dependency"
sources = ["journald"]
match_text = "Unmet dependencies"
severity = "warn"
title = "Package dependency conflict"
body = "apt can't resolve a dependency. Try: sudo apt --fix-broken install — this is more automatic than dnf's conflict resolution."
# ── AppArmor (replaces SELinux) ───────────────────────────────────────────────
[[patterns]]
id = "apparmor-denial"
sources = ["journald"]
match_text = "apparmor=\"DENIED\""
severity = "info"
title = "AppArmor access denied"
body = "Debian/Ubuntu ships AppArmor instead of SELinux. The concepts are similar but the tooling differs. Check: sudo aa-status — for audit logs: sudo aa-logprof — profiles are in /etc/apparmor.d/"
# ── System ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Firmware file missing"
body = "sudo apt install firmware-linux firmware-linux-nonfree — unlike Fedora where firmware comes via linux-firmware, Debian splits it into free/nonfree packages. Enable non-free in /etc/apt/sources.list first."
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "OOM killer fired"
body = "A process was killed for RAM. Ubuntu enables zswap by default; Debian doesn't always. Add a swapfile or enable zswap via /sys/module/zswap/parameters/enabled"
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Disk I/O error"
body = "Check SMART: sudo smartctl -a /dev/sdX — install: sudo apt install smartmontools"
# ── Audio ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "PipeWire not responding"
body = "Ubuntu 22.04+ and Debian 12+ ship PipeWire. Restart: systemctl --user restart pipewire pipewire-pulse wireplumber"
[[patterns]]
id = "pulseaudio-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to pulseaudio"
severity = "warn"
title = "PulseAudio not responding"
body = "Older Debian/Ubuntu systems use PulseAudio instead of PipeWire. Restart: pulseaudio --kill && pulseaudio --start"
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth rfkill blocked"
body = "rfkill unblock bluetooth — same as Fedora."
# ── GPU / display ─────────────────────────────────────────────────────────────
[[patterns]]
id = "gpu-hang"
sources = ["kmsg"]
match_text = "GPU HANG"
severity = "warn"
title = "GPU hang"
body = "GPU stopped responding. For NVIDIA on Debian/Ubuntu: sudo apt install nvidia-driver — or use ubuntu-drivers autoinstall on Ubuntu. Similar to Fedora's RPM Fusion approach."
# ── Network ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "NetworkManager: connection failed"
body = "nmcli device status — Debian may use ifupdown instead of NetworkManager on minimal installs. Check: systemctl status NetworkManager"
# ── Media ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "missing-codec"
sources = ["journald"]
match_text = "GStreamer: Failed to find plugin"
severity = "info"
title = "Missing media codec"
body = "On Ubuntu/Mint: sudo apt install ubuntu-restricted-extras — on Debian: enable non-free and install libavcodec-extra. Fedora's RPM Fusion serves the same purpose."
# ── Gaming ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "proton-runtime-missing"
sources = ["applog:proton"]
match_text = "wine: cannot find"
severity = "warn"
title = "Proton runtime issue"
body = "Right-click game in Steam -> Properties -> Local Files -> Verify integrity."

View file

@ -0,0 +1,130 @@
[meta]
source_os = "linux"
target_distro_family = "opensuse"
# Fedora/RHEL user moving to openSUSE Tumbleweed or Leap.
# Body text assumes DNF and RPM familiarity; both use RPM so tooling overlaps.
[log_paths]
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
# ── zypper / RPM ─────────────────────────────────────────────────────────────
[[patterns]]
id = "zypper-lock"
sources = ["journald"]
match_text = "System management is locked"
severity = "warn"
title = "zypper package manager is locked"
body = "Another zypper or PackageKit process is running — same situation as dnf being held by dnf-automatic. Wait it out or check: sudo ps aux | grep zypper"
[[patterns]]
id = "zypper-dep-conflict"
sources = ["journald"]
match_text = "conflicts with"
severity = "warn"
title = "Package dependency conflict"
body = "zypper presents conflicts interactively. Both Fedora's dnf and zypper use RPM, but zypper's solver can be more conservative. Try: sudo zypper dup (distribution upgrade) for more aggressive resolution."
[[patterns]]
id = "zypper-gpg-key"
sources = ["journald"]
match_text = "does not verify"
severity = "warn"
title = "Repository signature not trusted"
body = "Auto-import: sudo zypper --gpg-auto-import-keys ref — similar to dnf's GPG key prompts but the accept syntax differs."
# ── AppArmor (replaces SELinux) ───────────────────────────────────────────────
[[patterns]]
id = "apparmor-denial"
sources = ["journald"]
match_text = "apparmor=\"DENIED\""
severity = "info"
title = "AppArmor access denied"
body = "openSUSE ships AppArmor, not SELinux like Fedora. Similar purpose but different tooling. Check: sudo aa-status — audit: sudo aa-logprof — you'll need to rebuild your mental model from SELinux policy types to AppArmor profiles."
# ── System ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Firmware file missing"
body = "sudo zypper install kernel-firmware — openSUSE uses a single kernel-firmware package similar to Fedora's linux-firmware."
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "OOM killer fired"
body = "A process was killed for RAM. openSUSE prompts for swap setup during install. If skipped: sudo dd if=/dev/zero of=/swapfile bs=1M count=4096 && sudo mkswap /swapfile && sudo swapon /swapfile"
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Disk I/O error"
body = "Check SMART: sudo smartctl -a /dev/sdX — install: sudo zypper install smartmontools"
# ── YaST (openSUSE-specific) ──────────────────────────────────────────────────
[[patterns]]
id = "yast-backend-fail"
sources = ["journald"]
match_text = "YaST got signal"
severity = "warn"
title = "YaST configuration tool crashed"
body = "YaST is openSUSE's graphical admin tool (no Fedora equivalent). If it crashed mid-operation, check what it was doing: sudo yast2 -- the text mode version often recovers where the GUI fails."
# ── Audio ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "PipeWire not responding"
body = "Tumbleweed ships PipeWire like Fedora. Restart: systemctl --user restart pipewire pipewire-pulse wireplumber"
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth rfkill blocked"
body = "rfkill unblock bluetooth — same as Fedora."
# ── GPU / display ─────────────────────────────────────────────────────────────
[[patterns]]
id = "gpu-hang"
sources = ["kmsg"]
match_text = "GPU HANG"
severity = "warn"
title = "GPU hang"
body = "GPU stopped responding. For NVIDIA on openSUSE: use the NVIDIA OBS repo or packages.opensuse.org — similar to RPM Fusion on Fedora."
# ── Network ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "NetworkManager: connection failed"
body = "nmcli device status — openSUSE uses NetworkManager or Wicked depending on the install profile. Check which is active: systemctl status NetworkManager wicked"
# ── Gaming ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "proton-runtime-missing"
sources = ["applog:proton"]
match_text = "wine: cannot find"
severity = "warn"
title = "Proton runtime issue"
body = "Right-click game in Steam -> Properties -> Local Files -> Verify integrity. For Steam on openSUSE: sudo zypper install steam (from the games repo on OBS)."

View file

@ -0,0 +1,120 @@
[meta]
source_os = "ipad"
target_distro_family = "arch"
# iPad/iPhone user on their first Arch/CachyOS install.
# More sandboxed mental model than Android — no file manager, no sideloading concept,
# everything lived inside apps. Arch is a steep starting point; body text is encouraging
# but honest about the learning curve.
[log_paths]
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
# ── Package management ────────────────────────────────────────────────────────
[[patterns]]
id = "pacman-db-lock"
sources = ["journald", "applog:pacman"]
match_text = "could not lock database: File exists"
severity = "warn"
title = "App installer is busy"
body = "The package manager (pacman — Linux's version of the App Store, but text-based) got interrupted and left a lock file behind. Think of it like an App Store update that got cut off. If nothing is currently installing, remove the lock: open a terminal (called 'Konsole' or 'Alacritty' on your system) and type: sudo rm /var/lib/pacman/db.lck — then press Enter. 'sudo' means run as administrator; your password won't show as you type."
[[patterns]]
id = "partial-upgrade-warning"
sources = ["applog:pacman"]
match_text = "warning: database file for"
severity = "info"
title = "App list out of date — update everything together"
body = "On iPad, updates happen automatically and silently. On Arch Linux, you run them manually. Important rule: always update everything at once. In a terminal, type: sudo pacman -Syu — press Enter, enter your password. This refreshes the app list AND updates all installed software."
[[patterns]]
id = "aur-build-failure"
sources = ["journald", "applog:pacman"]
match_text = "error: failed to build"
severity = "warn"
title = "App build failed')"
body = "The AUR (Arch User Repository) is software that gets compiled on your machine from source code — there's no real iOS equivalent since Apple controls all distribution. A build failed. Look at the error text above this notification for the specific cause. The AUR package's comment page on aur.archlinux.org often has workarounds."
# ── Files and paths ───────────────────────────────────────────────────────────
[[patterns]]
id = "permission-denied"
sources = ["journald"]
match_text = "Permission denied"
severity = "info"
title = "Permission denied"
body = "On iPad, every app lives in its own private sandbox — you never think about file permissions. On Linux, files are shared between users and programs, and access is controlled by permissions. If you need admin rights for a command, add 'sudo' before it: sudo <command> — and enter your password."
[[patterns]]
id = "no-such-file"
sources = ["journald"]
match_text = "No such file or directory"
severity = "info"
title = "File or folder not found"
body = "On iPad, files lived inside apps and you never typed paths. On Linux, files have locations like /home/username/Documents. Check that the path you typed is correct — Linux paths are case-sensitive ('Documents' and 'documents' are different). Use 'ls' to list files in the current folder."
# ── System ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Hardware driver file missing"
body = "On iPad, Apple controls all hardware and drivers — you never see this. On Linux, some hardware components need a driver file installed separately. Install the main driver package: sudo pacman -S linux-firmware — then restart your computer."
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "System ran out of memory — closed an app"
body = "Linux had to close a program to free up RAM — similar to iPadOS refreshing apps in the background when RAM runs low. If this keeps happening, consider closing unused programs or adding 'zram' (uses storage as extra RAM): sudo pacman -S zram-generator"
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Storage error"
body = "Something went wrong with the drive. Install a check tool: sudo pacman -S smartmontools — then run: sudo smartctl -a /dev/sda"
# ── Audio ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "Sound system not responding"
body = "PipeWire manages audio on this system — like the iOS audio system, but you can restart it. Type in a terminal: systemctl --user restart pipewire pipewire-pulse wireplumber — if sound still doesn't work, log out and log back in."
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth turned off by software"
body = "A software setting is blocking Bluetooth — like enabling Airplane Mode. Turn it back on: rfkill unblock bluetooth — in a terminal. If it says 'Hard blocked', there's a physical switch on your computer or a setting in the BIOS."
# ── Network ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "Wi-Fi connection failed"
body = "Couldn't connect to the network. Check status: nmcli device status — in a terminal. If the Wi-Fi adapter doesn't appear in the list, the driver may not be loaded."
# ── GPU ───────────────────────────────────────────────────────────────────────
[[patterns]]
id = "gpu-hang"
sources = ["kmsg"]
match_text = "GPU HANG"
severity = "warn"
title = "Graphics card stopped responding"
body = "The graphics system crashed and recovered — similar to an app freezing on iPad, but at a lower level. If this keeps happening during games or video, update your graphics drivers: sudo pacman -Syu"

View file

@ -0,0 +1,137 @@
[meta]
source_os = "ipad"
target_distro_family = "debian"
# iPad/iPhone user on their first Debian/Ubuntu/Mint install.
# Ubuntu/Mint are the most recommended starting points for iPad migrants —
# automatic updates, GUI software center, familiar GNOME/Cinnamon interface.
[log_paths]
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
# ── Package management ────────────────────────────────────────────────────────
[[patterns]]
id = "apt-lock"
sources = ["journald"]
match_text = "Could not get lock /var/lib/dpkg/lock"
severity = "warn"
title = "Software installer is busy"
body = "Ubuntu's software installer is already running in the background — probably doing automatic updates, similar to how iOS updates apps silently. Wait a minute and try again. You can also open 'Software Updater' from the app menu to see what's happening. If it's been stuck for a long time: open the Terminal app and type: sudo rm /var/lib/dpkg/lock-frontend && sudo dpkg --configure -a"
[[patterns]]
id = "dpkg-interrupted"
sources = ["journald"]
match_text = "dpkg was interrupted"
severity = "warn"
title = "App install was cut short"
body = "A previous software install didn't finish. Fix it: open Terminal and type: sudo dpkg --configure -a — then press Enter."
[[patterns]]
id = "apt-unmet-dependency"
sources = ["journald"]
match_text = "Unmet dependencies"
severity = "warn"
title = "App needs another app first"
body = "The software you're installing needs something else first. Let Ubuntu fix it: sudo apt --fix-broken install"
# ── AppArmor ──────────────────────────────────────────────────────────────────
[[patterns]]
id = "apparmor-denial"
sources = ["journald"]
match_text = "apparmor=\"DENIED\""
severity = "info"
title = "App blocked by security policy"
body = "Ubuntu includes AppArmor — a security layer that restricts what each program can do, similar to how iOS tightly sandboxes every app. Something was blocked. This is usually routine security protection, not a problem."
# ── Files and paths ───────────────────────────────────────────────────────────
[[patterns]]
id = "permission-denied"
sources = ["journald"]
match_text = "Permission denied"
severity = "info"
title = "Permission denied"
body = "On iPad, files are hidden inside apps and permissions are invisible. On Linux, files are shared and controlled. If a command fails with this, add 'sudo' before it to run as admin: sudo <command> — your password won't show as you type, that's normal."
# ── System ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Hardware driver file missing"
body = "Some hardware needs a driver file installed separately — unlike iPad where Apple controls everything. Ubuntu usually handles this, but if something isn't working: sudo apt install firmware-linux linux-firmware — then restart."
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "System ran out of memory"
body = "Linux closed a program to free up RAM — similar to iPadOS refreshing apps in the background. Try closing some programs you're not using."
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Storage error"
body = "A hardware-level storage error. Install: sudo apt install smartmontools — then check: sudo smartctl -a /dev/sda"
# ── Audio ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "Sound system not responding"
body = "Restart audio: systemctl --user restart pipewire pipewire-pulse wireplumber — or log out and back in."
[[patterns]]
id = "pulseaudio-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to pulseaudio"
severity = "warn"
title = "Sound system not responding"
body = "Restart audio: pulseaudio --kill && pulseaudio --start — or log out and back in."
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth turned off by software"
body = "Run: rfkill unblock bluetooth — in a terminal."
[[patterns]]
id = "cups-server-error"
sources = ["journald"]
match_text = "Unable to connect to CUPS server"
severity = "info"
title = "Printer service not running"
body = "The printing service needs to be started: sudo systemctl start cups && sudo systemctl enable cups — then try printing again from your app."
# ── Network ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "Wi-Fi connection failed"
body = "Check the network status icon in the top bar — or open Terminal and type: nmcli device status"
# ── Media ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "missing-codec"
sources = ["journald"]
match_text = "GStreamer: Failed to find plugin"
severity = "info"
title = "Video or audio format not supported"
body = "Linux needs extra packages to play some media formats. On Ubuntu/Mint: sudo apt install ubuntu-restricted-extras — this adds support for MP3, MP4, and other common formats."

View file

@ -0,0 +1,102 @@
[meta]
source_os = "ipad"
target_distro_family = "fedora"
# iPad/iPhone user on their first Fedora install.
# Assumes NO terminal experience. Fedora GNOME is visually close to iPadOS.
[log_paths]
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
# ── Package management ────────────────────────────────────────────────────────
[[patterns]]
id = "dnf-lock"
sources = ["journald"]
match_text = "Another app is currently holding the dnf lock"
severity = "warn"
title = "Software installer is busy"
body = "Fedora is doing automatic updates in the background — like iOS updating apps silently. Wait a minute and try again. You can see what's happening in GNOME Software (the App Store equivalent)."
[[patterns]]
id = "dnf-dep-conflict"
sources = ["journald"]
match_text = "conflicts with"
severity = "warn"
title = "Two apps conflict with each other"
body = "Two packages need something that can't coexist. Run: sudo dnf distro-sync — in a terminal to bring everything back into sync."
# ── SELinux ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "selinux-denial"
sources = ["journald"]
match_text = "type=AVC"
severity = "info"
title = "Security system blocked an action"
body = "Fedora uses SELinux — a detailed security system that controls what each program can access. Think of it as a much stricter version of iOS app permissions. This log entry is usually routine. If an app keeps failing, GNOME's 'Setroubleshoot' tool will pop up with an explanation and fix."
# ── System ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Hardware driver file missing"
body = "Some hardware needs a driver installed separately. Fedora usually handles this automatically, but if a device isn't working: sudo dnf install linux-firmware — restart after."
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "System ran out of memory"
body = "Linux closed a program to free RAM — like iPadOS refreshing background apps. Close unused programs."
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Storage error"
body = "Install: sudo dnf install smartmontools — then check: sudo smartctl -a /dev/sda"
# ── Audio ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "Sound system not responding"
body = "Restart audio: systemctl --user restart pipewire pipewire-pulse wireplumber — or log out and back in."
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth turned off by software"
body = "Run: rfkill unblock bluetooth — in a terminal."
# ── Network ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "Wi-Fi connection failed"
body = "Check the network icon in the top bar, or: nmcli device status — in a terminal."
# ── Media ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "missing-codec"
sources = ["journald"]
match_text = "GStreamer: Failed to find plugin"
severity = "info"
title = "Video or audio format not supported"
body = "Fedora needs extra packages for some media formats. Enable RPM Fusion: sudo dnf install https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm — then: sudo dnf install gstreamer1-plugins-bad-free gstreamer1-plugins-ugly"

View file

@ -0,0 +1,111 @@
[meta]
source_os = "ipad"
target_distro_family = "opensuse"
# iPad/iPhone user on their first openSUSE install.
# YaST (openSUSE's graphical admin tool) is a good bridge for users
# unfamiliar with terminal-based system administration.
[log_paths]
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
# ── Package management ────────────────────────────────────────────────────────
[[patterns]]
id = "zypper-lock"
sources = ["journald"]
match_text = "System management is locked"
severity = "warn"
title = "Software installer is busy"
body = "openSUSE's software manager is running — like iOS doing background updates. Wait a minute, or open YaST -> Software -> Software Management to see what's happening."
[[patterns]]
id = "zypper-dep-conflict"
sources = ["journald"]
match_text = "conflicts with"
severity = "warn"
title = "Two apps conflict"
body = "Run a full update: sudo zypper dup — this resolves most conflicts."
# ── AppArmor ──────────────────────────────────────────────────────────────────
[[patterns]]
id = "apparmor-denial"
sources = ["journald"]
match_text = "apparmor=\"DENIED\""
severity = "info"
title = "App blocked by security policy"
body = "openSUSE uses AppArmor for security — similar to how iOS isolates apps. Something was blocked. YaST -> Security -> AppArmor Configuration shows active profiles."
# ── YaST ──────────────────────────────────────────────────────────────────────
[[patterns]]
id = "yast-backend-fail"
sources = ["journald"]
match_text = "YaST got signal"
severity = "warn"
title = "Settings tool crashed"
body = "YaST (openSUSE's settings tool) crashed. Try running it from a terminal: sudo yast2 — the text mode is more stable than the graphical version."
# ── System ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Hardware driver file missing"
body = "Install the firmware package: sudo zypper install kernel-firmware — restart after."
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "System ran out of memory"
body = "Linux closed a program to free RAM. YaST -> System -> Partitioner can add or resize swap space (overflow RAM stored on disk)."
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Storage error"
body = "Install: sudo zypper install smartmontools — then: sudo smartctl -a /dev/sda"
# ── Audio ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "Sound system not responding"
body = "Restart audio: systemctl --user restart pipewire pipewire-pulse wireplumber — or log out and back in."
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth turned off by software"
body = "Run: rfkill unblock bluetooth — or use YaST -> Network -> Bluetooth."
[[patterns]]
id = "cups-server-error"
sources = ["journald"]
match_text = "Unable to connect to CUPS server"
severity = "info"
title = "Printer service not running"
body = "Start printing: sudo systemctl start cups && sudo systemctl enable cups — or use YaST -> Hardware -> Printer."
# ── Network ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "Wi-Fi connection failed"
body = "Check YaST -> Network Settings — or: nmcli device status — in a terminal."

View file

@ -0,0 +1,219 @@
[meta]
source_os = "linux"
target_distro_family = "arch"
# Experienced Linux user on first Arch/CachyOS install.
# Body text assumes familiarity with the terminal, systemd, and package management.
# Explanations focus on Arch-specific divergence from Debian/Fedora conventions.
[log_paths]
pacman = "/var/log/pacman.log"
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
lutris = "~/.cache/lutris/logs/lutris.log"
# ── pacman / AUR / Chaotic-AUR ───────────────────────────────────────────────
[[patterns]]
id = "pacman-db-lock"
sources = ["journald", "applog:pacman"]
match_text = "could not lock database: File exists"
severity = "warn"
title = "pacman database locked"
body = "Lock file left behind: sudo rm /var/lib/pacman/db.lck — verify nothing is actually running first (fuser /var/lib/pacman/db.lck)."
[[patterns]]
id = "pacman-dep-conflict"
sources = ["journald", "applog:pacman"]
match_text = "conflicting dependencies"
severity = "warn"
title = "Dependency conflict"
body = "Unlike apt/dnf, pacman won't silently resolve conflicts — you have to decide. Read the conflict message; usually one package replaces another (e.g. pipewire-pulse replaces pulseaudio). Explicitly remove the conflicting package first."
[[patterns]]
id = "pacman-conflicting-files"
sources = ["journald", "applog:pacman"]
match_text = "error: failed to commit transaction (conflicting files)"
severity = "warn"
title = "Conflicting files on install"
body = "A file owned by another package is in the way. Either the package is already partially installed, or there's a leftover file. Check which package owns it: pacman -Qo /path/to/file — then remove the conflict manually or use --overwrite if you're sure."
[[patterns]]
id = "aur-build-failure"
sources = ["journald", "applog:pacman"]
match_text = "error: failed to build"
severity = "warn"
title = "AUR package build failed"
body = "makepkg failed. Read the full output — common causes: missing makedepends (check the PKGBUILD), upstream tarball moved (check AUR comments), or a bad patch. paru -Si <pkg> shows the full dependency list."
[[patterns]]
id = "aur-pgp-key"
sources = ["journald", "applog:pacman"]
match_text = "unknown public key"
severity = "warn"
title = "PGP key not in keyring"
body = "gpg --recv-keys <keyid> — or if the AUR package's PKGBUILD specifies validpgpkeys, import exactly those. Don't set SKIP_PGP_CHECK unless you trust the source."
[[patterns]]
id = "makepkg-missing-deps"
sources = ["journald", "applog:pacman"]
match_text = "Missing dependencies"
severity = "warn"
title = "AUR build dependencies missing"
body = "makepkg needs packages that aren't installed. paru/yay resolve makedepends automatically; if building manually, install them first: sudo pacman -S <dep> or paru -S <dep> for AUR deps."
[[patterns]]
id = "partial-upgrade-warning"
sources = ["applog:pacman"]
match_text = "warning: database file for"
severity = "info"
title = "Package database out of sync"
body = "Running pacman -Sy without -u is dangerous on Arch — partial upgrades break things. Always use pacman -Syu. This is the biggest Arch-specific rule coming from Debian or Fedora where partial syncs are fine."
[[patterns]]
id = "chaotic-aur-sig-fail"
sources = ["journald", "applog:pacman"]
match_text = "signature from"
severity = "warn"
title = "Package signature verification failed"
body = "A package signature isn't trusted. If it's from Chaotic-AUR: sudo pacman-key --recv-key 3056513887B78AEB --keyserver keyserver.ubuntu.com && sudo pacman-key --lsign-key 3056513887B78AEB — then retry."
# ── Kernel / DKMS (rolling release gotcha) ───────────────────────────────────
[[patterns]]
id = "dkms-build-fail"
sources = ["journald"]
match_text = "Error! Build of"
severity = "warn"
title = "DKMS module failed to build"
body = "A kernel module didn't compile after a kernel update. More common on CachyOS than on stable distros because the kernel ships with custom patches. Check: dkms status — then reinstall the relevant dkms package or wait for an AUR update."
[[patterns]]
id = "cachyos-kernel-module-fail"
sources = ["kmsg"]
match_text = "module verification failed"
severity = "warn"
title = "Kernel module signature mismatch"
body = "A module doesn't match the running kernel's signing key. On CachyOS this can happen with third-party modules after a cachyos-kernel update. Reinstall the dkms module package or check if a -cachyos suffixed build exists in the AUR."
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Firmware file missing"
body = "sudo pacman -S linux-firmware — if it's a specific device (e.g. Realtek wifi), check linux-firmware-qlogic or a dedicated AUR package."
# ── System ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "locale-not-set"
sources = ["journald"]
match_text = "Cannot set LC_ALL to default locale"
severity = "info"
title = "Locale not generated"
body = "Unlike Debian/Ubuntu, Arch doesn't generate locales automatically. Edit /etc/locale.gen (uncomment your locale), then run: sudo locale-gen — and set LANG in /etc/locale.conf."
[[patterns]]
id = "systemd-resolved-fail"
sources = ["journald"]
match_text = "Failed to set DNS configuration"
severity = "info"
title = "DNS configuration failed"
body = "systemd-resolved had trouble applying DNS settings. Check: resolvectl status — on Arch, /etc/resolv.conf should be a symlink to /run/systemd/resolve/stub-resolv.conf. If it's a plain file it may conflict."
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "OOM killer fired"
body = "A process was killed for RAM. CachyOS ships uksmd (userspace KSM) to help with this — check it's running: systemctl status uksmd. Also consider zram: sudo pacman -S zram-generator."
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Disk I/O error"
body = "Storage error on a block device. Check SMART: sudo smartctl -a /dev/sdX — or for NVMe: sudo nvme smart-log /dev/nvme0."
# ── Audio / Bluetooth ─────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "PipeWire not responding"
body = "systemctl --user restart pipewire pipewire-pulse wireplumber — if it keeps failing, check: systemctl --user status pipewire"
[[patterns]]
id = "wireplumber-fail"
sources = ["journald"]
match_text = "Failed to activate"
severity = "warn"
title = "WirePlumber activation error"
body = "systemctl --user restart wireplumber — if audio devices keep disappearing after suspend/resume, this is a known CachyOS/PipeWire interaction; check AUR for wireplumber-git."
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth rfkill blocked"
body = "rfkill unblock bluetooth — if hard-blocked, check BIOS or a physical switch."
[[patterns]]
id = "bluetooth-profile-unavailable"
sources = ["journald"]
match_text = "br-connection-profile-unavailable"
severity = "info"
title = "Bluetooth audio profile missing"
body = "Check pipewire-bluetooth is installed: pacman -Q pipewire-bluetooth — and that wireplumber is running. Some headsets need libspa-bluetooth."
# ── GPU / display ─────────────────────────────────────────────────────────────
[[patterns]]
id = "gpu-hang"
sources = ["kmsg"]
match_text = "GPU HANG"
severity = "warn"
title = "GPU hang"
body = "GPU stopped responding; driver recovered. On CachyOS with AMD: check if mesa-git (from Chaotic-AUR) is newer than the stable mesa and matches your kernel. On NVIDIA: check nvidia-dkms version vs kernel version."
[[patterns]]
id = "xwayland-crash"
sources = ["journald"]
match_text = "XWayland server terminated unexpectedly"
severity = "warn"
title = "XWayland crashed"
body = "X11 apps will be dead until you restart your session. If this is reproducible with a specific app, try WAYLAND_DISPLAY= to force it onto XWayland explicitly, or check for a Wayland-native version."
# ── Gaming ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "proton-runtime-missing"
sources = ["applog:proton"]
match_text = "wine: cannot find"
severity = "warn"
title = "Proton runtime issue"
body = "Verify game files via Steam, or check that the Steam runtime is intact: ~/.steam/root/ubuntu12_32/"
[[patterns]]
id = "lutris-wine-fail"
sources = ["applog:lutris"]
match_text = "Wine is not installed"
severity = "warn"
title = "Lutris: Wine not found"
body = "Lutris needs a Wine runner. In Lutris: Preferences -> Runners -> Wine -> Install — or install wine from the AUR: paru -S wine-staging"
# ── Network ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "NetworkManager: connection failed"
body = "nmcli device status — if a wifi adapter is missing, check dmesg for firmware errors. CachyOS ships most firmware in linux-firmware but some chips (Realtek 8852) need AUR packages."

View file

@ -0,0 +1,177 @@
[meta]
source_os = "macos"
target_distro_family = "arch"
[log_paths]
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
retroarch = "~/.config/retroarch/retroarch.log"
lutris = "~/.cache/lutris/logs/lutris.log"
# ── AUR / package management ────────────────────────────────────────────────
[[patterns]]
id = "aur-build-failure"
sources = ["journald"]
match_text = "error: failed to build"
severity = "warn"
title = "AUR package build failed"
body = "A package failed to compile from source. This usually means a missing dependency or a broken AUR package. Check the output for the specific error, or look up the package comments on aur.archlinux.org."
[[patterns]]
id = "aur-pgp-key"
sources = ["journald"]
match_text = "unknown public key"
severity = "warn"
title = "Missing PGP key for AUR package"
body = "The package signature can't be verified. The key ID will be in the error — run: gpg --recv-keys <keyid>"
[[patterns]]
id = "pacman-db-lock"
sources = ["journald"]
match_text = "could not lock database: File exists"
severity = "warn"
title = "Pacman database is locked"
body = "Another pacman process left a lock file behind. If nothing is running: sudo rm /var/lib/pacman/db.lck — then retry your command."
[[patterns]]
id = "pacman-dep-conflict"
sources = ["journald"]
match_text = "conflicting dependencies"
severity = "warn"
title = "Package dependency conflict"
body = "Two packages need incompatible versions of something. On Arch this sometimes happens with AUR packages. Read the conflict message carefully — one of the packages usually has to be replaced or manually removed first."
[[patterns]]
id = "dkms-build-fail"
sources = ["journald"]
match_text = "Error! Build of"
severity = "warn"
title = "Kernel module failed to build"
body = "A DKMS kernel module didn't compile after a kernel update. This can break hardware that needs out-of-tree drivers (e.g. some wifi cards, VirtualBox, NVIDIA). Check: dkms status — then reinstall the failing module package."
# ── Audio ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "Audio server not responding"
body = "An app can't reach PipeWire (the audio server). Try: systemctl --user restart pipewire pipewire-pulse — if that doesn't help, log out and back in."
[[patterns]]
id = "wireplumber-fail"
sources = ["journald"]
match_text = "Failed to activate"
severity = "warn"
title = "Audio session manager error"
body = "WirePlumber (the audio session manager) had a problem activating a device. Try: systemctl --user restart wireplumber — if audio devices keep disappearing, check that your user is in the 'audio' group: groups $USER"
# ── Bluetooth ────────────────────────────────────────────────────────────────
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth is blocked by rfkill"
body = "The system's radio kill switch is blocking Bluetooth. Run: rfkill list — if Bluetooth shows 'Hard blocked: no, Soft blocked: yes', run: rfkill unblock bluetooth"
[[patterns]]
id = "bluetooth-profile-unavailable"
sources = ["journald"]
match_text = "br-connection-profile-unavailable"
severity = "info"
title = "Bluetooth profile not available"
body = "A Bluetooth device connected but a required profile isn't available. This often means missing codecs. Try: sudo pacman -S pulseaudio-bluetooth (if using PulseAudio) or check that pipewire-bluetooth is installed."
# ── GPU / display ─────────────────────────────────────────────────────────────
[[patterns]]
id = "gpu-hang"
sources = ["kmsg"]
match_text = "GPU HANG"
severity = "warn"
title = "GPU hang detected"
body = "The graphics card stopped responding. Linux recovered, but this can cause game crashes and display glitches. Common causes: overheating, or a driver bug with the current kernel. Check GPU temperature and consider updating mesa or your GPU driver package."
[[patterns]]
id = "xwayland-crash"
sources = ["journald"]
match_text = "XWayland server terminated unexpectedly"
severity = "warn"
title = "XWayland crashed"
body = "The compatibility layer for older X11 apps crashed. Apps using X11 (not Wayland-native) will stop working until you restart your session. If this keeps happening, try running the affected app with WAYLAND_DISPLAY= cleared to force X11 mode."
# ── Hardware / kernel ─────────────────────────────────────────────────────────
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Missing firmware for hardware"
body = "Your system is missing a firmware file for a hardware component. On Arch: sudo pacman -S linux-firmware — then reboot."
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "System ran out of memory"
body = "Linux had to kill a process because RAM was exhausted. On macOS, the system compresses memory instead. On Linux you can add swap space as a safety net: consider a swapfile if your machine has limited RAM."
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Disk read/write error"
body = "A storage device had an I/O error. This can mean a failing drive, a bad cable, or a filesystem problem. Check the device with: sudo smartctl -a /dev/sdX — replace sdX with the device from the error."
# ── Network ──────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "Network connection failed to activate"
body = "NetworkManager couldn't connect to a network. Common causes: wrong wifi password, a driver issue, or the network requiring a login page. Check: nmcli device status"
# ── Media ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "missing-codec"
sources = ["journald"]
match_text = "GStreamer: Failed to find plugin"
severity = "info"
title = "Missing media codec"
body = "A media codec isn't installed. On Arch: sudo pacman -S gst-plugins-good gst-plugins-bad gst-libav"
# ── Gaming ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "proton-runtime-missing"
sources = ["applog:proton"]
match_text = "wine: cannot find"
severity = "warn"
title = "Proton runtime issue"
body = "Steam Proton couldn't find a required file. Try: right-click the game -> Properties -> Local Files -> Verify game files."
[[patterns]]
id = "steam-disk-write"
sources = ["applog:steam"]
match_text = "ERROR: failed to write"
severity = "warn"
title = "Steam disk write error"
body = "Steam can't write to its library folder. Check ownership: ls -la ~/.local/share/Steam"
[[patterns]]
id = "retroarch-shader-fail"
sources = ["applog:retroarch"]
match_text = "Failed to compile shader"
severity = "info"
title = "RetroArch shader failed to compile"
body = "A graphical shader couldn't load. Try switching preset in Settings -> Video -> Shaders."

View file

@ -0,0 +1,172 @@
[meta]
source_os = "linux"
target_distro_family = "arch"
# openSUSE Tumbleweed/Leap user moving to Arch.
# Body text assumes zypper, YaST, and AppArmor familiarity.
[log_paths]
pacman = "/var/log/pacman.log"
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
lutris = "~/.cache/lutris/logs/lutris.log"
# ── pacman / AUR ─────────────────────────────────────────────────────────────
[[patterns]]
id = "pacman-db-lock"
sources = ["journald", "applog:pacman"]
match_text = "could not lock database: File exists"
severity = "warn"
title = "pacman database locked"
body = "Lock file left from a crashed pacman run — like zypper's /var/run/zypp.pid getting orphaned. Remove if nothing is running: sudo rm /var/lib/pacman/db.lck"
[[patterns]]
id = "partial-upgrade-warning"
sources = ["applog:pacman"]
match_text = "warning: database file for"
severity = "info"
title = "Package database out of sync"
body = "On Arch, syncing without upgrading is dangerous — unlike openSUSE where zypper ref is safe to run alone. Always: pacman -Syu — never pacman -Sy without the u."
[[patterns]]
id = "pacman-dep-conflict"
sources = ["journald", "applog:pacman"]
match_text = "conflicting dependencies"
severity = "warn"
title = "Dependency conflict"
body = "Unlike zypper which resolves conflicts interactively, pacman puts the choice directly on you. Read the conflict — usually one package replaces another. Remove the conflicting package first."
[[patterns]]
id = "aur-build-failure"
sources = ["journald", "applog:pacman"]
match_text = "error: failed to build"
severity = "warn"
title = "AUR package build failed"
body = "The AUR has no binary packages — makepkg compiles from source every time. There's no OBS equivalent here. Check the build log and the AUR comments page for the package."
[[patterns]]
id = "aur-pgp-key"
sources = ["journald", "applog:pacman"]
match_text = "unknown public key"
severity = "warn"
title = "PGP key not in keyring"
body = "Import the key: gpg --recv-keys <keyid> — different from zypper's --gpg-auto-import-keys; this is GnuPG's personal keyring used by makepkg."
[[patterns]]
id = "chaotic-aur-sig-fail"
sources = ["journald", "applog:pacman"]
match_text = "signature from"
severity = "warn"
title = "Package signature verification failed"
body = "Chaotic-AUR key not trusted. Import: sudo pacman-key --recv-key 3056513887B78AEB --keyserver keyserver.ubuntu.com && sudo pacman-key --lsign-key 3056513887B78AEB"
# ── AppArmor → none ───────────────────────────────────────────────────────────
[[patterns]]
id = "apparmor-remnant"
sources = ["journald"]
match_text = "apparmor=\"DENIED\""
severity = "info"
title = "AppArmor log entry (no AppArmor on Arch by default)"
body = "Arch doesn't ship AppArmor by default. If you see this, you may have installed apparmor from the AUR manually. Run: sudo systemctl disable apparmor if you don't need it."
# ── Kernel / DKMS ─────────────────────────────────────────────────────────────
[[patterns]]
id = "dkms-build-fail"
sources = ["journald"]
match_text = "Error! Build of"
severity = "warn"
title = "DKMS module failed to build"
body = "A kernel module didn't compile. Arch's rolling kernel updates more frequently than Tumbleweed's. Check: dkms status — reinstall the failing dkms package."
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Firmware file missing"
body = "sudo pacman -S linux-firmware — equivalent to openSUSE's kernel-firmware."
# ── System ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "locale-not-set"
sources = ["journald"]
match_text = "Cannot set LC_ALL to default locale"
severity = "info"
title = "Locale not generated"
body = "Unlike YaST which configured this for you, Arch requires manual locale setup. Edit /etc/locale.gen, uncomment your locale, run: sudo locale-gen — set LANG in /etc/locale.conf."
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "OOM killer fired"
body = "Arch doesn't set up swap by default like openSUSE's installer does. Add zram: sudo pacman -S zram-generator — or add a swapfile."
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Disk I/O error"
body = "Check SMART: sudo smartctl -a /dev/sdX — install: sudo pacman -S smartmontools"
# ── Audio ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "PipeWire not responding"
body = "Both openSUSE and Arch ship PipeWire. Restart: systemctl --user restart pipewire pipewire-pulse wireplumber"
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth rfkill blocked"
body = "rfkill unblock bluetooth"
# ── GPU / display ─────────────────────────────────────────────────────────────
[[patterns]]
id = "gpu-hang"
sources = ["kmsg"]
match_text = "GPU HANG"
severity = "warn"
title = "GPU hang"
body = "GPU stopped responding. On Arch: AMD uses mesa (official repos), NVIDIA uses nvidia or nvidia-dkms. No OBS equivalent — everything is in pacman or the AUR."
[[patterns]]
id = "xwayland-crash"
sources = ["journald"]
match_text = "XWayland server terminated unexpectedly"
severity = "warn"
title = "XWayland crashed"
body = "X11 apps dead until session restart. Same behavior as on openSUSE Wayland sessions."
# ── Network ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "NetworkManager: connection failed"
body = "nmcli device status — Arch uses NetworkManager (not Wicked). If a wifi adapter is missing, check dmesg for firmware errors."
# ── Gaming ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "proton-runtime-missing"
sources = ["applog:proton"]
match_text = "wine: cannot find"
severity = "warn"
title = "Proton runtime issue"
body = "Right-click game in Steam -> Properties -> Local Files -> Verify integrity."

View file

@ -0,0 +1,138 @@
[meta]
source_os = "linux"
target_distro_family = "debian"
# openSUSE Tumbleweed/Leap user moving to Debian/Ubuntu/Mint.
# Body text assumes zypper, YaST, and AppArmor familiarity.
[log_paths]
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
# ── apt / dpkg ────────────────────────────────────────────────────────────────
[[patterns]]
id = "apt-lock"
sources = ["journald"]
match_text = "Could not get lock /var/lib/dpkg/lock"
severity = "warn"
title = "Package manager is locked"
body = "Another apt process is running — often unattended-upgrades (no zypper equivalent, but similar to PackageKit background updates). Wait a minute. If stuck: sudo rm /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock && sudo dpkg --configure -a"
[[patterns]]
id = "dpkg-interrupted"
sources = ["journald"]
match_text = "dpkg was interrupted"
severity = "warn"
title = "Package install was interrupted"
body = "Like a zypper transaction that got killed, but dpkg needs manual recovery. Fix: sudo dpkg --configure -a"
[[patterns]]
id = "apt-unmet-dependency"
sources = ["journald"]
match_text = "Unmet dependencies"
severity = "warn"
title = "Package dependency conflict"
body = "apt auto-resolves most conflicts — less interactive than zypper's conflict wizard. Let it try: sudo apt --fix-broken install"
# ── AppArmor ──────────────────────────────────────────────────────────────────
[[patterns]]
id = "apparmor-denial"
sources = ["journald"]
match_text = "apparmor=\"DENIED\""
severity = "info"
title = "AppArmor access denied"
body = "Both openSUSE and Debian/Ubuntu use AppArmor — the tooling is the same. Check: sudo aa-status — audit: sudo aa-logprof — profiles: /etc/apparmor.d/"
# ── System ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Firmware file missing"
body = "On Debian: sudo apt install firmware-linux firmware-linux-nonfree (enable non-free sources first). On Ubuntu: sudo apt install linux-firmware. Debian splits firmware by license unlike openSUSE's single kernel-firmware package."
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "OOM killer fired"
body = "A process was killed for RAM. openSUSE's installer sets up swap; Debian minimal may not. Add a swapfile: sudo fallocate -l 4G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile"
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Disk I/O error"
body = "Check SMART: sudo smartctl -a /dev/sdX — install: sudo apt install smartmontools"
# ── Audio ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "PipeWire not responding"
body = "Both Tumbleweed and Ubuntu 22.04+/Debian 12+ ship PipeWire. Restart: systemctl --user restart pipewire pipewire-pulse wireplumber"
[[patterns]]
id = "pulseaudio-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to pulseaudio"
severity = "warn"
title = "PulseAudio not responding"
body = "Older Debian systems still use PulseAudio. Restart: pulseaudio --kill && pulseaudio --start"
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth rfkill blocked"
body = "rfkill unblock bluetooth — same as openSUSE."
# ── GPU / display ─────────────────────────────────────────────────────────────
[[patterns]]
id = "gpu-hang"
sources = ["kmsg"]
match_text = "GPU HANG"
severity = "warn"
title = "GPU hang"
body = "GPU stopped responding. For NVIDIA on Ubuntu: ubuntu-drivers autoinstall — on Debian: apt install nvidia-driver (requires non-free). Unlike openSUSE's OBS NVIDIA repo, Ubuntu keeps drivers in the main archive."
# ── Network ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "NetworkManager: connection failed"
body = "nmcli device status — Debian minimal may use ifupdown instead of NetworkManager. Install if missing: sudo apt install network-manager"
# ── Printing ──────────────────────────────────────────────────────────────────
[[patterns]]
id = "cups-server-error"
sources = ["journald"]
match_text = "Unable to connect to CUPS server"
severity = "info"
title = "Printer service not running"
body = "sudo systemctl start cups && sudo systemctl enable cups — YaST auto-configured printing on openSUSE; Debian leaves CUPS disabled until you enable it."
# ── Gaming ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "proton-runtime-missing"
sources = ["applog:proton"]
match_text = "wine: cannot find"
severity = "warn"
title = "Proton runtime issue"
body = "Right-click game in Steam -> Properties -> Local Files -> Verify integrity."

View file

@ -0,0 +1,146 @@
[meta]
source_os = "linux"
target_distro_family = "fedora"
# openSUSE Tumbleweed/Leap user moving to Fedora.
# Body text assumes zypper, YaST, and AppArmor familiarity; both use RPM.
[log_paths]
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
# ── DNF / RPM ────────────────────────────────────────────────────────────────
[[patterns]]
id = "dnf-lock"
sources = ["journald"]
match_text = "Another app is currently holding the dnf lock"
severity = "warn"
title = "DNF package manager is locked"
body = "dnf-automatic (Fedora's background updater) is probably running — similar to PackageKit on openSUSE. Wait it out or check: sudo ps aux | grep dnf"
[[patterns]]
id = "dnf-dep-conflict"
sources = ["journald"]
match_text = "conflicts with"
severity = "warn"
title = "Package dependency conflict"
body = "Both use RPM but their solvers differ. DNF auto-resolves more aggressively than zypper. If dnf can't fix it: sudo dnf distro-sync — the equivalent of zypper dup."
[[patterns]]
id = "dnf-gpg-key"
sources = ["journald"]
match_text = "GPG key retrieval failed"
severity = "warn"
title = "Repository GPG key missing"
body = "Import: sudo rpm --import /path/to/key.gpg — same rpm command as openSUSE. RPM Fusion keys are imported automatically when you enable the repo."
# ── SELinux (replaces AppArmor) ───────────────────────────────────────────────
[[patterns]]
id = "selinux-denial"
sources = ["journald"]
match_text = "type=AVC"
severity = "info"
title = "SELinux access denied"
body = "Fedora uses SELinux instead of openSUSE's AppArmor. Both are MAC systems but with different models — SELinux uses type enforcement, AppArmor uses path-based profiles. Check: ausearch -m AVC -ts recent — get a fix: sealert -a /var/log/audit/audit.log"
[[patterns]]
id = "selinux-context-wrong"
sources = ["journald"]
match_text = "restorecon"
severity = "info"
title = "SELinux file context mismatch"
body = "Files copied from openSUSE or an external drive may have wrong SELinux labels. Fix: sudo restorecon -Rv /path/to/file — equivalent to aa-relabel in AppArmor terms."
# ── System ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Firmware file missing"
body = "sudo dnf install linux-firmware — same scope as openSUSE's kernel-firmware. Some chips need RPM Fusion nonfree: sudo dnf install rpmfusion-nonfree-release-$(rpm -E %fedora)"
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "OOM killer fired"
body = "A process was killed for RAM. Fedora enables zswap by default on modern releases. For zram: sudo dnf install zram-generator — similar setup to openSUSE."
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Disk I/O error"
body = "Check SMART: sudo smartctl -a /dev/sdX — install: sudo dnf install smartmontools"
# ── YaST → no YaST ───────────────────────────────────────────────────────────
[[patterns]]
id = "yast-not-found"
sources = ["journald"]
match_text = "yast: command not found"
severity = "info"
title = "YaST not available on Fedora"
body = "Fedora has no YaST equivalent — use GNOME Settings for display/network/user config, and dnf/rpm for package management. Most things YaST handled are done via systemctl, nmcli, or the GNOME control center."
# ── Audio ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "PipeWire not responding"
body = "Both Tumbleweed and Fedora ship PipeWire. Restart: systemctl --user restart pipewire pipewire-pulse wireplumber"
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth rfkill blocked"
body = "rfkill unblock bluetooth — same as openSUSE."
# ── GPU / display ─────────────────────────────────────────────────────────────
[[patterns]]
id = "gpu-hang"
sources = ["kmsg"]
match_text = "GPU HANG"
severity = "warn"
title = "GPU hang"
body = "GPU stopped responding. For NVIDIA on Fedora: sudo dnf install akmod-nvidia (from RPM Fusion) — similar to openSUSE's NVIDIA OBS repo but uses akmods instead of DKMS."
[[patterns]]
id = "xwayland-crash"
sources = ["journald"]
match_text = "XWayland server terminated unexpectedly"
severity = "warn"
title = "XWayland crashed"
body = "Fedora GNOME defaults to Wayland like openSUSE's GNOME spin. X11 apps dead until session restart."
# ── Network ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "NetworkManager: connection failed"
body = "nmcli device status — Fedora uses NetworkManager, not Wicked. If you had Wicked-specific configs on openSUSE, recreate them in NetworkManager format."
# ── Gaming ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "proton-runtime-missing"
sources = ["applog:proton"]
match_text = "wine: cannot find"
severity = "warn"
title = "Proton runtime issue"
body = "Right-click game in Steam -> Properties -> Local Files -> Verify integrity. Steam on Fedora: sudo dnf install steam (from RPM Fusion free)."

View file

@ -0,0 +1,187 @@
[meta]
source_os = "windows"
target_distro_family = "debian"
[log_paths]
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
retroarch = "~/.config/retroarch/retroarch.log"
libreoffice = "~/.config/libreoffice/4/user/registrymodifications.xcu"
# ── Package management ───────────────────────────────────────────────────────
[[patterns]]
id = "apt-lock"
sources = ["journald"]
match_text = "Could not get lock /var/lib/dpkg/lock"
severity = "warn"
title = "Package manager is locked"
body = "Another process is using apt — usually an automatic update running in the background. Wait a minute and try again. If it's been stuck a long time: sudo rm /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock"
[[patterns]]
id = "apt-unmet-dependency"
sources = ["journald"]
match_text = "Unmet dependencies"
severity = "warn"
title = "Package dependency conflict"
body = "apt can't resolve a dependency. Try: sudo apt --fix-broken install"
[[patterns]]
id = "dpkg-interrupted"
sources = ["journald"]
match_text = "dpkg was interrupted"
severity = "warn"
title = "Package install was interrupted"
body = "A previous install didn't complete cleanly. This is like a Windows installer that got cut off mid-run. Fix it with: sudo dpkg --configure -a — then run your install again."
# ── Audio ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "Audio server not responding"
body = "An app can't reach PipeWire (the audio system). Try: systemctl --user restart pipewire pipewire-pulse — if sound still doesn't work, log out and back in."
[[patterns]]
id = "pulseaudio-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to pulseaudio"
severity = "warn"
title = "Audio server not responding"
body = "An app can't reach the audio server. Try: pulseaudio --kill && pulseaudio --start — if that doesn't help, log out and back in."
# ── Bluetooth ────────────────────────────────────────────────────────────────
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth is software-blocked"
body = "A software switch is blocking Bluetooth. Run: rfkill unblock bluetooth — if it's showing Hard blocked, there may be a physical switch or BIOS setting involved."
[[patterns]]
id = "bluetooth-profile-unavailable"
sources = ["journald"]
match_text = "br-connection-profile-unavailable"
severity = "info"
title = "Bluetooth profile not available"
body = "A Bluetooth device connected but a required audio profile isn't available. Try: sudo apt install pulseaudio-module-bluetooth — then restart pulseaudio."
# ── Filesystem / storage ──────────────────────────────────────────────────────
[[patterns]]
id = "ntfs-volume-dirty"
sources = ["kmsg"]
match_text = "volume is dirty"
severity = "warn"
title = "External drive needs Windows check"
body = "An NTFS drive (probably from Windows) wasn't safely ejected and needs a check. Mount it in Windows and run chkdsk, or force-mount on Linux with: sudo mount -o remove_hiberfile /dev/sdX /mnt/point"
[[patterns]]
id = "ntfs-force-required"
sources = ["kmsg"]
match_text = "Dirty flag is set"
severity = "warn"
title = "Drive mounted read-only (dirty flag)"
body = "Linux mounted this NTFS drive read-only because Windows marked it as needing a check. Boot into Windows and do a safe shutdown, or use: sudo ntfsfix /dev/sdX"
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Disk read/write error"
body = "A storage device had an error. This could be a failing USB drive, a bad cable, or a corrupted filesystem. Check: sudo smartctl -a /dev/sdX — replace sdX with the device shown in the error."
[[patterns]]
id = "usb-device-reset"
sources = ["kmsg"]
match_text = "device descriptor read/64, error"
severity = "info"
title = "USB device not recognised"
body = "A USB device is having trouble connecting. Try a different USB port, or unplug and replug. If it's a hub, try plugging directly into the computer."
# ── Hardware / kernel ─────────────────────────────────────────────────────────
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Missing firmware for hardware"
body = "Your system is missing a firmware file. On Debian/Ubuntu/Mint: sudo apt install firmware-linux firmware-linux-nonfree — then reboot."
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "System ran out of memory"
body = "Linux had to forcibly close a program to free RAM. Windows handles this differently with virtual memory. If this keeps happening, consider adding a swap file or closing more background apps."
[[patterns]]
id = "gpu-hang"
sources = ["kmsg"]
match_text = "GPU HANG"
severity = "warn"
title = "GPU hang detected"
body = "The graphics card stopped responding. Linux recovered, but games or video apps may have crashed. Check for overheating, and make sure your GPU drivers are up to date."
# ── Network ──────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "Network connection failed"
body = "NetworkManager couldn't connect. Common causes: wrong wifi password, a captive portal (hotel/coffee shop wifi), or a driver issue. Check: nmcli device status"
# ── Printing ──────────────────────────────────────────────────────────────────
[[patterns]]
id = "cups-server-error"
sources = ["journald"]
match_text = "Unable to connect to CUPS server"
severity = "info"
title = "Printer service not running"
body = "The print server (CUPS) isn't running. Start it: sudo systemctl start cups — and enable it to start automatically: sudo systemctl enable cups"
# ── Media ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "missing-codec"
sources = ["journald"]
match_text = "GStreamer: Failed to find plugin"
severity = "info"
title = "Missing media codec"
body = "A media codec isn't installed. On Ubuntu/Mint: sudo apt install ubuntu-restricted-extras — this installs common video, audio, and font packages."
[[patterns]]
id = "snap-confinement"
sources = ["journald"]
match_text = "snap: cannot use strict"
severity = "info"
title = "Snap package permission issue"
body = "A Snap package is having permission trouble. Try running it with --devmode, or look for a Flatpak or apt alternative. Note: Snap is disabled by default on Linux Mint — use apt or Flatpak instead."
# ── Gaming ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "proton-runtime-missing"
sources = ["applog:proton"]
match_text = "wine: cannot find"
severity = "warn"
title = "Proton runtime issue"
body = "Steam Proton couldn't find a required file. Right-click the game -> Properties -> Local Files -> Verify game files."
[[patterns]]
id = "steam-disk-write"
sources = ["applog:steam"]
match_text = "ERROR: failed to write"
severity = "warn"
title = "Steam disk write error"
body = "Steam can't write to its library folder. Check: ls -la ~/.local/share/Steam"

View file

@ -1,6 +1,6 @@
use crate::config::{RobinConfig, MigrationConfig, SourceOs}; use crate::config::{MigrationConfig, NotificationLevel, RobinConfig, SourceOs};
use tauri::State;
use std::sync::Mutex; use std::sync::Mutex;
use tauri::{Emitter, State};
pub struct AppState { pub struct AppState {
pub config: Mutex<RobinConfig>, pub config: Mutex<RobinConfig>,
@ -8,14 +8,18 @@ pub struct AppState {
#[tauri::command] #[tauri::command]
pub fn get_config(state: State<'_, AppState>) -> Result<RobinConfig, String> { pub fn get_config(state: State<'_, AppState>) -> Result<RobinConfig, String> {
state.config.lock() state
.config
.lock()
.map(|c| c.clone()) .map(|c| c.clone())
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
} }
#[tauri::command] #[tauri::command]
pub fn needs_onboarding(state: State<'_, AppState>) -> bool { pub fn needs_onboarding(state: State<'_, AppState>) -> bool {
state.config.lock() state
.config
.lock()
.map(|c| c.needs_onboarding()) .map(|c| c.needs_onboarding())
.unwrap_or(true) .unwrap_or(true)
} }
@ -24,20 +28,119 @@ pub fn needs_onboarding(state: State<'_, AppState>) -> bool {
pub fn complete_onboarding( pub fn complete_onboarding(
source_os: String, source_os: String,
distro: String, distro: String,
source_distro: Option<String>,
dual_boot_with: Option<String>,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<(), String> { ) -> Result<(), String> {
let source = match source_os.to_lowercase().as_str() { let source = match source_os.to_lowercase().as_str() {
"macos" | "mac" => SourceOs::Macos, "macos" | "mac" => SourceOs::Macos,
"windows" => SourceOs::Windows, "windows" => SourceOs::Windows,
"linux" => SourceOs::Linux, "linux" => SourceOs::Linux,
"android" => SourceOs::Android,
"ipad" | "ios" | "ipados" => SourceOs::IpadOs,
_ => SourceOs::Unknown, _ => SourceOs::Unknown,
}; };
let detected = if distro == "unknown" || distro.is_empty() {
crate::distro::detect()
} else {
distro
};
let source_distro_family = source_distro.as_deref().and_then(|sd| {
let family = crate::distro::distro_family(sd);
if family == "unknown" { None } else { Some(family.to_string()) }
});
// Normalise dual_boot_with to a canonical name; reject unrecognised values.
let dual_boot_with = dual_boot_with.and_then(|s| match s.to_lowercase().as_str() {
"windows" => Some("windows".to_string()),
"macos" | "mac" => Some("macos".to_string()),
_ => None,
});
let mut config = state.config.lock().map_err(|e| e.to_string())?; let mut config = state.config.lock().map_err(|e| e.to_string())?;
config.migration = Some(MigrationConfig { config.migration = Some(MigrationConfig {
source_os: source, source_os: source,
distro, distro: detected,
source_distro_family,
dual_boot_with,
fluency_level: 0, fluency_level: 0,
}); });
config.save().map_err(|e| e.to_string()) config.save().map_err(|e| e.to_string())
} }
#[tauri::command]
pub fn update_notification_level(level: String, state: State<'_, AppState>) -> Result<(), String> {
let parsed = match level.as_str() {
"off" => NotificationLevel::Off,
"badge_only" => NotificationLevel::BadgeOnly,
"badge_and_toast" => NotificationLevel::BadgeAndToast,
_ => return Err(format!("unknown notification level: {level}")),
};
let mut config = state.config.lock().map_err(|e| e.to_string())?;
config.display.notification_level = parsed;
config.save().map_err(|e| e.to_string())
}
#[tauri::command]
pub fn get_pending_events() -> Vec<crate::patterns::MatchedEvent> {
crate::notify::take_pending()
}
#[tauri::command]
pub fn panel_opened() {
crate::notify::set_panel_open(true);
}
#[tauri::command]
pub fn panel_closed() {
crate::notify::set_panel_open(false);
}
#[tauri::command]
pub async fn chat(
message: String,
state: State<'_, AppState>,
app_handle: tauri::AppHandle,
) -> Result<(), String> {
let (base_url, model, source_os, distro) = {
let cfg = state.config.lock().map_err(|e| e.to_string())?;
let (source_os, distro) = if let Some(ref m) = cfg.migration {
let os = match m.source_os {
crate::config::SourceOs::Macos => "macOS",
crate::config::SourceOs::Windows => "Windows",
crate::config::SourceOs::Linux => "Linux",
crate::config::SourceOs::Android => "Android",
crate::config::SourceOs::IpadOs => "iPad/iOS",
crate::config::SourceOs::Unknown => "Unknown",
};
(os.to_string(), m.distro.clone())
} else {
("Unknown".to_string(), "unknown".to_string())
};
(
cfg.ollama.base_url.clone(),
cfg.ollama.model.clone(),
source_os,
distro,
)
};
let system_prompt = crate::llm::build_system_prompt(&source_os, &distro);
let messages = vec![
crate::llm::ChatMessage { role: "system".into(), content: system_prompt },
crate::llm::ChatMessage { role: "user".into(), content: message },
];
tauri::async_runtime::spawn(async move {
if let Err(e) = crate::llm::chat_stream(&base_url, &model, messages, &app_handle).await {
log::error!("chat stream error: {e}");
if let Err(emit_err) = app_handle.emit("robin:chat-error", e.to_string()) {
log::warn!("failed to emit robin:chat-error: {emit_err}");
}
}
});
Ok(())
}

View file

@ -2,20 +2,49 @@ use anyhow::{Context, Result};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::PathBuf; use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum SourceOs { pub enum SourceOs {
Macos, Macos,
Windows, Windows,
Linux, Linux,
Android,
IpadOs,
Unknown, Unknown,
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "snake_case")]
pub enum Tier {
#[default]
Free,
Paid,
Premium,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "snake_case")]
pub enum NotificationLevel {
Off,
BadgeOnly,
#[default]
BadgeAndToast,
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MigrationConfig { pub struct MigrationConfig {
pub source_os: SourceOs, pub source_os: SourceOs,
/// Detected distro string, e.g. "cachyos", "linuxmint" /// Detected distro string, e.g. "cachyos", "linuxmint"
pub distro: String, pub distro: String,
/// Source distro family for Linux-to-Linux migrations, e.g. "debian", "fedora", "arch", "opensuse"
/// Used to load a more specific pattern file (e.g. debian-to-arch) before the generic linux-to-arch fallback.
#[serde(default)]
pub source_distro_family: Option<String>,
/// Co-installed OS for dual-boot setups, e.g. "windows", "macos".
/// When set, a supplementary pattern file (dualboot-windows.toml) is loaded on top of
/// the primary migration patterns to surface coexistence-specific issues.
#[serde(default)]
pub dual_boot_with: Option<String>,
/// 05: grows as user dismisses suggestions they already know /// 05: grows as user dismisses suggestions they already know
pub fluency_level: u8, pub fluency_level: u8,
} }
@ -37,14 +66,15 @@ impl Default for OllamaConfig {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DisplayConfig { pub struct DisplayConfig {
pub show_notifications: bool, #[serde(default)]
pub notification_level: NotificationLevel,
pub quiet_mode: bool, pub quiet_mode: bool,
} }
impl Default for DisplayConfig { impl Default for DisplayConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
show_notifications: true, notification_level: NotificationLevel::BadgeAndToast,
quiet_mode: false, quiet_mode: false,
} }
} }
@ -55,6 +85,8 @@ pub struct RobinConfig {
pub migration: Option<MigrationConfig>, pub migration: Option<MigrationConfig>,
pub ollama: OllamaConfig, pub ollama: OllamaConfig,
pub display: DisplayConfig, pub display: DisplayConfig,
#[serde(default)]
pub tier: Tier,
} }
impl Default for RobinConfig { impl Default for RobinConfig {
@ -63,6 +95,7 @@ impl Default for RobinConfig {
migration: None, migration: None,
ollama: OllamaConfig::default(), ollama: OllamaConfig::default(),
display: DisplayConfig::default(), display: DisplayConfig::default(),
tier: Tier::Free,
} }
} }
} }
@ -80,8 +113,7 @@ impl RobinConfig {
} }
let content = std::fs::read_to_string(&path) let content = std::fs::read_to_string(&path)
.with_context(|| format!("failed to read {}", path.display()))?; .with_context(|| format!("failed to read {}", path.display()))?;
toml::from_str(&content) toml::from_str(&content).with_context(|| format!("failed to parse {}", path.display()))
.with_context(|| format!("failed to parse {}", path.display()))
} }
pub fn save(&self) -> Result<()> { pub fn save(&self) -> Result<()> {
@ -117,6 +149,8 @@ mod tests {
config.migration = Some(MigrationConfig { config.migration = Some(MigrationConfig {
source_os: SourceOs::Macos, source_os: SourceOs::Macos,
distro: "cachyos".into(), distro: "cachyos".into(),
source_distro_family: None,
dual_boot_with: None,
fluency_level: 0, fluency_level: 0,
}); });
assert!(!config.needs_onboarding()); assert!(!config.needs_onboarding());
@ -128,6 +162,8 @@ mod tests {
migration: Some(MigrationConfig { migration: Some(MigrationConfig {
source_os: SourceOs::Windows, source_os: SourceOs::Windows,
distro: "linuxmint".into(), distro: "linuxmint".into(),
source_distro_family: None,
dual_boot_with: None,
fluency_level: 2, fluency_level: 2,
}), }),
..Default::default() ..Default::default()
@ -136,4 +172,36 @@ mod tests {
let deserialized: RobinConfig = toml::from_str(&serialized).unwrap(); let deserialized: RobinConfig = toml::from_str(&serialized).unwrap();
assert!(!deserialized.needs_onboarding()); assert!(!deserialized.needs_onboarding());
} }
#[test]
fn notification_level_default_is_badge_and_toast() {
let config = RobinConfig::default();
assert!(matches!(
config.display.notification_level,
NotificationLevel::BadgeAndToast
));
}
#[test]
fn tier_default_is_free() {
let config = RobinConfig::default();
assert!(matches!(config.tier, Tier::Free));
}
#[test]
fn notification_level_roundtrips_toml() {
let config = RobinConfig {
display: DisplayConfig {
notification_level: NotificationLevel::BadgeOnly,
quiet_mode: false,
},
..Default::default()
};
let toml = toml::to_string_pretty(&config).unwrap();
let back: RobinConfig = toml::from_str(&toml).unwrap();
assert!(matches!(
back.display.notification_level,
NotificationLevel::BadgeOnly
));
}
} }

88
src-tauri/src/distro.rs Normal file
View file

@ -0,0 +1,88 @@
pub fn detect() -> String {
std::fs::read_to_string("/etc/os-release")
.map(|s| parse_id(&s))
.unwrap_or_else(|_| "unknown".to_string())
}
pub fn distro_family(id: &str) -> &'static str {
match id {
"arch" | "cachyos" | "endeavouros" | "manjaro" | "garuda" => "arch",
"debian" | "ubuntu" | "linuxmint" | "mint" | "pop" | "elementary" | "kali" => "debian",
"fedora" | "rhel" | "centos" | "rocky" | "alma" => "fedora",
"opensuse" | "opensuse-tumbleweed" | "opensuse-leap" => "opensuse",
_ => "unknown",
}
}
fn parse_id(content: &str) -> String {
content
.lines()
.find_map(|line| {
let (key, val) = line.split_once('=')?;
if key.trim() == "ID" {
let raw = val.trim();
let unquoted = raw
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.or_else(|| raw.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
.unwrap_or(raw);
Some(unquoted.to_lowercase())
} else {
None
}
})
.unwrap_or_else(|| "unknown".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_arch_id() {
let content = "ID=arch\nID_LIKE=\nPRETTY_NAME=Arch Linux\n";
assert_eq!(parse_id(content), "arch");
}
#[test]
fn parses_cachyos_id() {
let content = "ID=cachyos\nID_LIKE=arch\nPRETTY_NAME=CachyOS\n";
assert_eq!(parse_id(content), "cachyos");
}
#[test]
fn parses_linuxmint_id() {
let content = "ID=linuxmint\nID_LIKE=ubuntu\nPRETTY_NAME=Linux Mint 22\n";
assert_eq!(parse_id(content), "linuxmint");
}
#[test]
fn distro_family_arch_based() {
assert_eq!(distro_family("cachyos"), "arch");
assert_eq!(distro_family("arch"), "arch");
assert_eq!(distro_family("manjaro"), "arch");
}
#[test]
fn distro_family_debian_based() {
assert_eq!(distro_family("linuxmint"), "debian");
assert_eq!(distro_family("ubuntu"), "debian");
assert_eq!(distro_family("debian"), "debian");
}
#[test]
fn distro_family_unknown() {
assert_eq!(distro_family("slackware"), "unknown");
}
#[test]
fn parses_single_quoted_id() {
let content = "ID='arch'\nPRETTY_NAME='Arch Linux'\n";
assert_eq!(parse_id(content), "arch");
}
#[test]
fn distro_family_mint_legacy() {
assert_eq!(distro_family("mint"), "debian");
}
}

View file

@ -1,11 +1,16 @@
mod commands; mod commands;
mod config; mod config;
mod distro;
mod llm;
mod notify;
mod patterns;
mod tray; mod tray;
mod watcher; mod watcher;
use commands::AppState; use commands::AppState;
use config::RobinConfig; use config::RobinConfig;
use std::sync::Mutex; use std::sync::{Arc, Mutex};
use tauri::Manager;
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
@ -21,13 +26,83 @@ pub fn run() {
}) })
.setup(|app| { .setup(|app| {
tray::build_tray(&app.handle())?; tray::build_tray(&app.handle())?;
watcher::spawn();
let state = app.state::<AppState>();
let cfg = state
.config
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone();
let pattern_file = if let Some(ref migration) = cfg.migration {
let family = distro::distro_family(&migration.distro);
let source = match migration.source_os {
config::SourceOs::Macos => "macos",
config::SourceOs::Windows => "windows",
config::SourceOs::Linux => "linux",
config::SourceOs::Android => "android",
config::SourceOs::IpadOs => "ipad",
config::SourceOs::Unknown => "unknown",
};
log::info!("robin: loading patterns for {source} → {family} (distro: {})", migration.distro);
let mut pf = match patterns::load(source, migration.source_distro_family.as_deref(), family) {
Ok(p) => {
log::info!("robin: loaded {} patterns", p.patterns.len());
Some(p)
}
Err(e) => {
log::warn!("robin: no pattern file found ({e}) — notifications disabled");
None
}
};
// Layer dual-boot supplement patterns on top when a co-installed OS is configured.
if let (Some(ref mut primary), Some(ref dualboot)) = (&mut pf, &migration.dual_boot_with) {
match patterns::load_supplement(dualboot) {
Ok(supplement) => {
log::info!("robin: loaded {} dual-boot supplement patterns for {dualboot}", supplement.patterns.len());
primary.extend(supplement);
}
Err(e) => log::warn!("robin: dual-boot supplement not found for {dualboot}: {e}"),
}
}
pf
} else {
log::info!("robin: no migration config — onboarding needed");
None
};
let log_paths = pattern_file
.as_ref()
.map(|pf| pf.log_paths.clone())
.unwrap_or_default();
let rx = watcher::spawn(log_paths);
let pf = Arc::new(pattern_file);
let app_handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
let mut rx = rx;
while let Some(event) = rx.recv().await {
if let Some(ref pf) = *pf {
if let Some(matched) = patterns::classify(&event, pf) {
log::info!("robin: matched pattern '{}' — dispatching notification", matched.pattern_id);
notify::dispatch(&app_handle, matched);
}
}
}
});
Ok(()) Ok(())
}) })
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
commands::get_config, commands::get_config,
commands::needs_onboarding, commands::needs_onboarding,
commands::complete_onboarding, commands::complete_onboarding,
commands::update_notification_level,
commands::get_pending_events,
commands::panel_opened,
commands::panel_closed,
commands::chat,
]) ])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running Robin"); .expect("error while running Robin");

157
src-tauri/src/llm.rs Normal file
View file

@ -0,0 +1,157 @@
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use tauri::Emitter;
#[derive(Debug, Clone, Serialize)]
pub struct ChatMessage {
pub role: String,
pub content: String,
}
#[derive(Debug, Deserialize)]
struct OllamaChunk {
message: Option<OllamaChunkMessage>,
#[serde(default)]
done: bool,
}
#[derive(Debug, Deserialize)]
struct OllamaChunkMessage {
content: String,
}
/// Build a migration-aware system prompt from the user's source OS and current distro.
pub fn build_system_prompt(source_os: &str, distro: &str) -> String {
format!(
"You are Robin, a friendly Linux migration assistant running locally on the user's machine. \
The user is migrating from {source_os} to {distro}. \
Help them with Linux questions, explain differences from their previous OS, \
and provide practical command-line solutions. \
Be concise, accurate, and use examples when helpful. \
Never suggest Windows or macOS solutions focus on Linux."
)
}
/// Stream a chat response from Ollama, emitting tokens as Tauri events.
///
/// Emits: `robin:chat-token` (String), `robin:chat-done` (unit), `robin:chat-error` (String).
pub async fn chat_stream(
base_url: &str,
model: &str,
messages: Vec<ChatMessage>,
app: &tauri::AppHandle,
) -> Result<()> {
let client = reqwest::Client::new();
let url = format!("{base_url}/api/chat");
let body = serde_json::json!({
"model": model,
"messages": messages,
"stream": true,
});
let response = client
.post(&url)
.json(&body)
.send()
.await
.with_context(|| format!("failed to connect to Ollama at {url}"))?;
if !response.status().is_success() {
let status = response.status();
let text = response.text().await.unwrap_or_default();
anyhow::bail!("Ollama returned {status}: {text}");
}
// Buffer bytes across chunks to handle UTF-8 sequences split at chunk boundaries.
let mut buf: Vec<u8> = Vec::new();
let mut response = response;
while let Some(chunk) = response.chunk().await.context("stream read error")? {
buf.extend_from_slice(&chunk);
// Extract complete newline-delimited JSON lines from the buffer.
while let Some(pos) = buf.iter().position(|&b| b == b'\n') {
let line_bytes: Vec<u8> = buf.drain(..=pos).collect();
let line = String::from_utf8_lossy(&line_bytes);
let line = line.trim();
if line.is_empty() {
continue;
}
match serde_json::from_str::<OllamaChunk>(line) {
Ok(chunk) => {
if let Some(msg) = chunk.message {
if !msg.content.is_empty() {
if let Err(e) = app.emit("robin:chat-token", msg.content) {
log::warn!("failed to emit robin:chat-token: {e}");
}
}
}
if chunk.done {
if let Err(e) = app.emit("robin:chat-done", ()) {
log::warn!("failed to emit robin:chat-done: {e}");
}
return Ok(());
}
}
Err(e) => {
log::warn!("failed to parse Ollama chunk '{}': {e}", line);
}
}
}
}
// Stream ended without a done=true chunk — emit done anyway so frontend unblocks.
if let Err(e) = app.emit("robin:chat-done", ()) {
log::warn!("failed to emit robin:chat-done at stream end: {e}");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn system_prompt_contains_source_and_distro() {
let prompt = build_system_prompt("Windows", "cachyos");
assert!(prompt.contains("Windows"));
assert!(prompt.contains("cachyos"));
}
#[test]
fn system_prompt_contains_robin() {
let prompt = build_system_prompt("macOS", "arch");
assert!(prompt.to_lowercase().contains("robin"));
}
#[test]
fn ollama_chunk_parses_token() {
let json = r#"{"model":"llama3.2","message":{"role":"assistant","content":"Hello"},"done":false}"#;
let chunk: OllamaChunk = serde_json::from_str(json).unwrap();
assert_eq!(chunk.message.unwrap().content, "Hello");
assert!(!chunk.done);
}
#[test]
fn ollama_chunk_parses_done() {
let json = r#"{"model":"llama3.2","message":{"role":"assistant","content":""},"done":true}"#;
let chunk: OllamaChunk = serde_json::from_str(json).unwrap();
assert!(chunk.done);
}
#[test]
fn ollama_chunk_parses_no_message() {
let json = r#"{"model":"llama3.2","done":true}"#;
let chunk: OllamaChunk = serde_json::from_str(json).unwrap();
assert!(chunk.message.is_none());
assert!(chunk.done);
}
#[test]
fn malformed_json_fails_to_parse() {
let json = r#"{"not_valid": }"#;
let result = serde_json::from_str::<OllamaChunk>(json);
assert!(result.is_err());
}
}

View file

@ -2,5 +2,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() { fn main() {
app_lib::run(); robin_lib::run();
} }

95
src-tauri/src/notify.rs Normal file
View file

@ -0,0 +1,95 @@
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
use tauri::{AppHandle, Emitter, Manager, Runtime};
use tauri_plugin_notification::NotificationExt;
use crate::commands::AppState;
use crate::config::NotificationLevel;
use crate::patterns::MatchedEvent;
static PENDING: Mutex<Vec<MatchedEvent>> = Mutex::new(vec![]);
// True while ChatPanel is mounted and listening for live events.
// When true, dispatch skips PENDING so events are not shown twice on re-open.
static PANEL_OPEN: AtomicBool = AtomicBool::new(false);
pub fn set_panel_open(open: bool) {
PANEL_OPEN.store(open, Ordering::Relaxed);
}
pub fn dispatch<R: Runtime>(app: &AppHandle<R>, event: MatchedEvent) {
let level = app
.state::<AppState>()
.config
.lock()
.map(|c| c.display.notification_level.clone())
.unwrap_or_default();
if !PANEL_OPEN.load(Ordering::Relaxed) {
match PENDING.lock() {
Ok(mut pending) => pending.push(event.clone()),
Err(e) => log::warn!("notify: pending queue lock poisoned — event dropped: {e}"),
}
}
match level {
NotificationLevel::Off => {}
NotificationLevel::BadgeOnly => {
crate::tray::badge_on(app);
}
NotificationLevel::BadgeAndToast => {
crate::tray::badge_on(app);
send_toast(app, &event);
}
}
let _ = app.emit("robin:event", &event);
}
fn send_toast<R: Runtime>(app: &AppHandle<R>, event: &MatchedEvent) {
let _ = app
.notification()
.builder()
.title(&event.title)
.body(&event.body)
.show();
}
pub fn take_pending() -> Vec<MatchedEvent> {
PENDING
.lock()
.map(|mut v| std::mem::take(&mut *v))
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
fn make_event(id: &str) -> MatchedEvent {
MatchedEvent {
pattern_id: id.into(),
title: "Title".into(),
body: "Body".into(),
severity: "warn".into(),
timestamp: 0,
}
}
#[test]
fn take_pending_drains_queue() {
// Ensure clean state — drain any events from other tests sharing the static
let _ = take_pending();
if let Ok(mut q) = PENDING.lock() {
q.push(make_event("a"));
q.push(make_event("b"));
}
let first = take_pending();
assert_eq!(first.len(), 2);
assert_eq!(first[0].pattern_id, "a");
let second = take_pending();
assert!(second.is_empty(), "queue must be empty after drain");
}
}

334
src-tauri/src/patterns.rs Normal file
View file

@ -0,0 +1,334 @@
use crate::watcher::{EventSource, SystemEvent};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Deserialize)]
pub struct PatternMeta {
pub source_os: String,
pub target_distro_family: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Pattern {
pub id: String,
pub sources: Vec<String>,
pub match_text: String,
pub severity: String,
pub title: String,
pub body: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PatternFile {
pub meta: PatternMeta,
#[serde(default)]
pub log_paths: HashMap<String, String>,
#[serde(default)]
pub patterns: Vec<Pattern>,
}
#[derive(Debug, Clone, Serialize)]
pub struct MatchedEvent {
pub pattern_id: String,
pub title: String,
pub body: String,
pub severity: String,
pub timestamp: u64,
}
/// Load the pattern file for a source OS and distro family.
///
/// For Linux-to-Linux migrations, `source_distro_family` (e.g. "debian", "fedora") is
/// tried first: `debian-to-arch.toml` before the generic `linux-to-arch.toml` fallback.
/// Tries each candidate at three path depths: dev-relative, src-tauri-relative, system.
pub fn load(
source_os: &str,
source_distro_family: Option<&str>,
distro_family: &str,
) -> Result<PatternFile> {
let mut candidates: Vec<String> = Vec::new();
if let Some(src_distro) = source_distro_family {
let specific = format!("{src_distro}-to-{distro_family}.toml");
candidates.push(format!("patterns/{specific}"));
candidates.push(format!("src-tauri/patterns/{specific}"));
candidates.push(format!("/usr/share/robin/patterns/{specific}"));
}
let generic = format!("{source_os}-to-{distro_family}.toml");
candidates.push(format!("patterns/{generic}"));
candidates.push(format!("src-tauri/patterns/{generic}"));
candidates.push(format!("/usr/share/robin/patterns/{generic}"));
for path in &candidates {
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(_) => continue,
};
let pf: PatternFile = match toml::from_str(&content) {
Ok(p) => p,
Err(e) => {
log::warn!("patterns: failed to parse {path}: {e}");
continue;
}
};
for p in &pf.patterns {
anyhow::ensure!(
!p.match_text.is_empty(),
"pattern '{}' has empty match_text in {path}",
p.id
);
anyhow::ensure!(
!p.sources.is_empty(),
"pattern '{}' has empty sources list in {path}",
p.id
);
}
return Ok(pf);
}
anyhow::bail!("pattern file not found for {source_os}-to-{distro_family}.toml")
}
impl PatternFile {
/// Merge patterns and log_paths from `other` into this file.
/// Used to layer a dual-boot supplement on top of the primary pattern file.
pub fn extend(&mut self, other: PatternFile) {
self.patterns.extend(other.patterns);
for (k, v) in other.log_paths {
self.log_paths.entry(k).or_insert(v);
}
}
}
/// Load a supplementary pattern file by name (e.g. "windows" loads `dualboot-windows.toml`).
/// Supplement files cover coexistence-specific issues and are merged into the primary file.
pub fn load_supplement(name: &str) -> Result<PatternFile> {
let filename = format!("dualboot-{name}.toml");
let candidates = [
format!("patterns/{filename}"),
format!("src-tauri/patterns/{filename}"),
format!("/usr/share/robin/patterns/{filename}"),
];
for path in &candidates {
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(_) => continue,
};
match toml::from_str::<PatternFile>(&content) {
Ok(pf) => return Ok(pf),
Err(e) => {
log::warn!("patterns: failed to parse supplement {path}: {e}");
continue;
}
}
}
anyhow::bail!("supplement not found: {filename}")
}
#[must_use]
pub fn classify(event: &SystemEvent, pf: &PatternFile) -> Option<MatchedEvent> {
for pattern in &pf.patterns {
if !event.raw_line.contains(&pattern.match_text) {
continue;
}
let source_matches = pattern.sources.iter().any(|s| {
if s == "any" {
return true;
}
match &event.source {
EventSource::Journald => s == "journald",
EventSource::Kmsg => s == "kmsg",
EventSource::AppLog { app } => s == &format!("applog:{app}"),
}
});
if source_matches {
return Some(MatchedEvent {
pattern_id: pattern.id.clone(),
title: pattern.title.clone(),
body: pattern.body.clone(),
severity: pattern.severity.clone(),
timestamp: event.timestamp,
});
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::watcher::{EventSource, SystemEvent};
const FIXTURE: &str = r#"
[meta]
source_os = "macos"
target_distro_family = "arch"
[log_paths]
steam = "~/.local/share/Steam/logs/content_log.txt"
[[patterns]]
id = "aur-build-failure"
sources = ["journald"]
match_text = "error: failed to build"
severity = "warn"
title = "AUR package build failed"
body = "A package failed to compile from source."
[[patterns]]
id = "proton-runtime"
sources = ["applog:steam"]
match_text = "wine: cannot find"
severity = "warn"
title = "Proton runtime issue"
body = "Steam Proton couldn't find a required file."
[[patterns]]
id = "any-source-pattern"
sources = ["any"]
match_text = "kernel panic"
severity = "crit"
title = "Kernel panic"
body = "The kernel encountered a fatal error."
"#;
fn make_event(source: EventSource, raw_line: &str) -> SystemEvent {
SystemEvent {
source,
raw_line: raw_line.into(),
timestamp: 0,
}
}
#[test]
fn loads_pattern_file_from_toml() {
let pf: PatternFile = toml::from_str(FIXTURE).unwrap();
assert_eq!(pf.meta.source_os, "macos");
assert_eq!(pf.patterns.len(), 3);
assert_eq!(
pf.log_paths.get("steam").unwrap(),
"~/.local/share/Steam/logs/content_log.txt"
);
}
#[test]
fn classify_matches_journald_event() {
let pf: PatternFile = toml::from_str(FIXTURE).unwrap();
let event = make_event(EventSource::Journald, ":: error: failed to build (foo-git)");
let matched = classify(&event, &pf).unwrap();
assert_eq!(matched.pattern_id, "aur-build-failure");
assert_eq!(matched.title, "AUR package build failed");
}
#[test]
fn classify_no_match_returns_none() {
let pf: PatternFile = toml::from_str(FIXTURE).unwrap();
let event = make_event(
EventSource::Journald,
"systemd: Started NetworkManager.service",
);
assert!(classify(&event, &pf).is_none());
}
#[test]
fn classify_applog_matches_correct_source() {
let pf: PatternFile = toml::from_str(FIXTURE).unwrap();
let event = make_event(
EventSource::AppLog {
app: "steam".into(),
},
"wine: cannot find /run/media/user/game.exe",
);
let matched = classify(&event, &pf).unwrap();
assert_eq!(matched.pattern_id, "proton-runtime");
}
#[test]
fn classify_does_not_match_wrong_source() {
let pf: PatternFile = toml::from_str(FIXTURE).unwrap();
// proton pattern is applog:steam — journald event must not match
let event = make_event(EventSource::Journald, "wine: cannot find something");
assert!(classify(&event, &pf).is_none());
}
#[test]
fn classify_applog_does_not_match_different_app() {
let pf: PatternFile = toml::from_str(FIXTURE).unwrap();
let event = make_event(
EventSource::AppLog {
app: "retroarch".into(),
},
"wine: cannot find libvulkan.so",
);
assert!(classify(&event, &pf).is_none());
}
#[test]
fn classify_any_source_matches_kmsg() {
let pf: PatternFile = toml::from_str(FIXTURE).unwrap();
let event = make_event(EventSource::Kmsg, "kernel panic - not syncing: VFS");
let matched = classify(&event, &pf).unwrap();
assert_eq!(matched.pattern_id, "any-source-pattern");
}
#[test]
fn classify_any_source_matches_journald() {
let pf: PatternFile = toml::from_str(FIXTURE).unwrap();
let event = make_event(EventSource::Journald, "kernel panic - not syncing: VFS");
let matched = classify(&event, &pf).unwrap();
assert_eq!(matched.pattern_id, "any-source-pattern");
}
#[test]
fn load_rejects_empty_match_text() {
let bad = r#"
[meta]
source_os = "macos"
target_distro_family = "arch"
[[patterns]]
id = "bad"
sources = ["journald"]
match_text = ""
severity = "warn"
title = "Bad"
body = "Bad pattern."
"#;
let pf: PatternFile = toml::from_str(bad).unwrap();
// Validate manually since load() reads from filesystem; test the invariant directly
let result: anyhow::Result<()> = (|| {
for p in &pf.patterns {
anyhow::ensure!(!p.match_text.is_empty(), "empty match_text in '{}'", p.id);
}
Ok(())
})();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("empty match_text"));
}
#[test]
fn load_rejects_empty_sources() {
let bad = r#"
[meta]
source_os = "macos"
target_distro_family = "arch"
[[patterns]]
id = "bad"
sources = []
match_text = "something"
severity = "warn"
title = "Bad"
body = "Bad pattern."
"#;
let pf: PatternFile = toml::from_str(bad).unwrap();
let result: anyhow::Result<()> = (|| {
for p in &pf.patterns {
anyhow::ensure!(!p.sources.is_empty(), "empty sources in '{}'", p.id);
}
Ok(())
})();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("empty sources"));
}
}

View file

@ -1,3 +1,4 @@
use std::sync::atomic::{AtomicBool, Ordering};
use tauri::{ use tauri::{
menu::{Menu, MenuItem}, menu::{Menu, MenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
@ -13,7 +14,7 @@ pub fn build_tray<R: Runtime>(app: &AppHandle<R>) -> tauri::Result<()> {
.tooltip("Robin") .tooltip("Robin")
.icon(app.default_window_icon().cloned().unwrap()) .icon(app.default_window_icon().cloned().unwrap())
.menu(&menu) .menu(&menu)
.menu_on_left_click(false) .show_menu_on_left_click(false)
.on_menu_event(|app, event| match event.id.as_ref() { .on_menu_event(|app, event| match event.id.as_ref() {
"open" => toggle_chat_panel(app), "open" => toggle_chat_panel(app),
"quit" => app.exit(0), "quit" => app.exit(0),
@ -34,7 +35,7 @@ pub fn build_tray<R: Runtime>(app: &AppHandle<R>) -> tauri::Result<()> {
Ok(()) Ok(())
} }
fn toggle_chat_panel<R: Runtime>(app: &AppHandle<R>) { pub fn toggle_chat_panel<R: Runtime>(app: &AppHandle<R>) {
if let Some(window) = app.get_webview_window("chat") { if let Some(window) = app.get_webview_window("chat") {
if window.is_visible().unwrap_or(false) { if window.is_visible().unwrap_or(false) {
let _ = window.hide(); let _ = window.hide();
@ -44,3 +45,32 @@ fn toggle_chat_panel<R: Runtime>(app: &AppHandle<R>) {
} }
} }
} }
static BADGE_ACTIVE: AtomicBool = AtomicBool::new(false);
pub fn badge_on<R: Runtime>(app: &AppHandle<R>) {
if BADGE_ACTIVE.swap(true, Ordering::Relaxed) {
return; // already badged
}
if let Some(tray) = app.tray_by_id("robin-tray") {
if let Some(icon) = app.default_window_icon().cloned() {
let _ = tray.set_icon(Some(icon));
}
let _ = tray.set_tooltip(Some("Robin — something to show you"));
}
}
pub fn badge_off<R: Runtime>(app: &AppHandle<R>) {
BADGE_ACTIVE.store(false, Ordering::Relaxed);
if let Some(tray) = app.tray_by_id("robin-tray") {
if let Some(icon) = app.default_window_icon().cloned() {
let _ = tray.set_icon(Some(icon));
}
let _ = tray.set_tooltip(Some("Robin"));
}
}
pub fn clear_badge_and_open<R: Runtime>(app: &AppHandle<R>) {
badge_off(app);
toggle_chat_panel(app);
}

View file

@ -1,32 +0,0 @@
/// System event watcher — M1 implementation.
///
/// M0 stub: defines the types and the spawn interface so the rest of the app
/// can wire up event handling now. Actual journald/dmesg reading lands in M1.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EventSeverity {
Info,
Warn,
Crit,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemEvent {
pub severity: EventSeverity,
pub source: String,
pub message: String,
pub timestamp: u64,
}
/// Starts the background watcher task.
/// M0: no-op placeholder — returns immediately.
/// M1: spawns a tokio task reading journald + dmesg and emitting events.
pub fn spawn() {
// TODO(M1): spawn tokio::task reading journald via sd-journal crate
// TODO(M1): spawn dmesg poller for kernel messages
// TODO(M1): emit SystemEvent via tauri app_handle.emit()
log::info!("watcher: stub — no-op until M1");
}

View file

@ -0,0 +1,124 @@
use super::{now_unix, EventSource, SystemEvent};
use notify::{recommended_watcher, RecursiveMode, Watcher};
use std::collections::HashMap;
use std::io::{Read, Seek};
use tokio::sync::mpsc;
pub async fn watch(log_paths: HashMap<String, String>, tx: mpsc::Sender<SystemEvent>) {
if log_paths.is_empty() {
return;
}
let expanded: HashMap<String, String> = log_paths
.iter()
.map(|(k, v)| (k.clone(), expand_tilde(v)))
.collect();
tokio::task::spawn_blocking(move || {
let (notify_tx, notify_rx) = std::sync::mpsc::channel();
let mut watcher = match recommended_watcher(notify_tx) {
Ok(w) => w,
Err(e) => {
log::error!("inotify watcher init failed: {e}");
return;
}
};
// positions: path -> (app_name, byte_offset)
let mut positions: HashMap<String, (String, u64)> = HashMap::new();
for (app_name, path) in &expanded {
let pb = std::path::Path::new(path);
if pb.exists() {
let len = std::fs::metadata(pb).map(|m| m.len()).unwrap_or(0);
positions.insert(path.clone(), (app_name.clone(), len));
watcher.watch(pb, RecursiveMode::NonRecursive).ok();
}
}
for result in notify_rx {
if let Ok(event) = result {
for path in event.paths {
let path_str = path.to_string_lossy().to_string();
if let Some((app_name, pos)) = positions.get_mut(&path_str) {
let (lines, new_pos) = read_new_lines(&path_str, *pos);
*pos = new_pos;
for line in lines {
tx.blocking_send(SystemEvent {
source: EventSource::AppLog {
app: app_name.clone(),
},
raw_line: line,
timestamp: now_unix(),
})
.ok();
}
}
}
}
}
})
.await
.ok();
}
pub fn expand_tilde(path: &str) -> String {
if let Some(rest) = path.strip_prefix("~/") {
let home = std::env::var("HOME").unwrap_or_else(|_| "/home/user".into());
format!("{home}/{rest}")
} else {
path.to_string()
}
}
pub fn read_new_lines(path: &str, from_byte: u64) -> (Vec<String>, u64) {
let mut file = match std::fs::File::open(path) {
Ok(f) => f,
Err(_) => return (vec![], from_byte),
};
if file.seek(std::io::SeekFrom::Start(from_byte)).is_err() {
return (vec![], from_byte);
}
let mut raw = Vec::new();
let bytes_read = file.read_to_end(&mut raw).unwrap_or(0);
let content = String::from_utf8_lossy(&raw).into_owned();
let new_pos = from_byte + bytes_read as u64;
let lines: Vec<String> = content
.lines()
.filter(|l| !l.is_empty())
.map(|l| l.to_string())
.collect();
(lines, new_pos)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn expand_tilde_home() {
let home = std::env::var("HOME").unwrap_or("/home/user".into());
let expanded = expand_tilde("~/.config/retroarch/retroarch.log");
assert!(expanded.starts_with(&home));
assert!(expanded.ends_with(".config/retroarch/retroarch.log"));
}
#[test]
fn expand_tilde_no_tilde() {
let path = "/absolute/path/to/file.log";
assert_eq!(expand_tilde(path), path);
}
#[tokio::test]
async fn reads_new_lines_from_file() {
use std::io::Write;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.log");
let mut f = std::fs::File::create(&path).unwrap();
write!(f, "line one\n").unwrap();
let (lines, pos) = read_new_lines(path.to_str().unwrap(), 0);
assert_eq!(lines, vec!["line one".to_string()]);
assert!(pos > 0);
}
}

View file

@ -0,0 +1,74 @@
use super::{now_unix, EventSource, SystemEvent};
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command;
use tokio::sync::mpsc;
pub async fn watch(tx: mpsc::Sender<SystemEvent>) {
let mut child = match Command::new("journalctl")
.args(["--follow", "--output=json", "--lines=0"])
.stdout(std::process::Stdio::piped())
.spawn()
{
Ok(c) => c,
Err(e) => {
log::error!("journald watcher: failed to spawn journalctl: {e}");
return;
}
};
let stdout = match child.stdout.take() {
Some(s) => s,
None => return,
};
let mut lines = BufReader::new(stdout).lines();
while let Ok(Some(line)) = lines.next_line().await {
if let Some(msg) = extract_message(&line) {
let _ = tx
.send(SystemEvent {
source: EventSource::Journald,
raw_line: msg,
timestamp: now_unix(),
})
.await;
}
}
log::warn!("journald watcher: journalctl exited — no new journal events will be observed");
let _ = child.wait().await;
}
fn extract_message(line: &str) -> Option<String> {
let json: serde_json::Value = serde_json::from_str(line).ok()?;
let msg = json.get("MESSAGE")?.as_str()?;
if msg.is_empty() {
return None;
}
Some(msg.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_message_from_journald_json() {
let line = r#"{"MESSAGE":"AUR build failed for foo","PRIORITY":"3","_COMM":"makepkg"}"#;
assert_eq!(
extract_message(line),
Some("AUR build failed for foo".to_string())
);
}
#[test]
fn extract_message_missing_returns_none() {
let line = r#"{"PRIORITY":"6","_COMM":"systemd"}"#;
assert_eq!(extract_message(line), None);
}
#[test]
fn extract_message_empty_skipped() {
let line = r#"{"MESSAGE":"","PRIORITY":"6"}"#;
assert_eq!(extract_message(line), None);
}
}

View file

@ -0,0 +1,68 @@
use super::{now_unix, EventSource, SystemEvent};
use tokio::fs::File;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::sync::mpsc;
pub async fn watch(tx: mpsc::Sender<SystemEvent>) {
let file = match File::open("/dev/kmsg").await {
Ok(f) => f,
Err(e) => {
log::warn!("kmsg watcher: cannot open /dev/kmsg: {e}");
return;
}
};
let mut lines = BufReader::new(file).lines();
while let Ok(Some(line)) = lines.next_line().await {
if let Some(msg) = parse_kmsg(&line) {
let _ = tx
.send(SystemEvent {
source: EventSource::Kmsg,
raw_line: msg,
timestamp: now_unix(),
})
.await;
}
}
}
fn parse_kmsg(line: &str) -> Option<String> {
let msg = if let Some(pos) = line.find(';') {
&line[pos + 1..]
} else {
line
};
if msg.is_empty() {
return None;
}
Some(msg.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_kmsg_line_with_semicolon() {
let line = "6,1234,56789,-;usb 1-1: new full-speed USB device number 2";
assert_eq!(
parse_kmsg(line),
Some("usb 1-1: new full-speed USB device number 2".to_string())
);
}
#[test]
fn parses_kmsg_line_without_semicolon() {
let line = "plain message without header";
assert_eq!(
parse_kmsg(line),
Some("plain message without header".to_string())
);
}
#[test]
fn skips_empty_message_after_semicolon() {
let line = "6,1234,56789,-;";
assert_eq!(parse_kmsg(line), None);
}
}

View file

@ -0,0 +1,85 @@
pub mod inotify;
pub mod journald;
pub mod kmsg;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::sync::mpsc;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EventSeverity {
Info,
Warn,
Crit,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum EventSource {
Journald,
Kmsg,
AppLog { app: String },
}
#[derive(Debug, Clone, Serialize)]
pub struct SystemEvent {
pub source: EventSource,
pub raw_line: String,
pub timestamp: u64,
}
pub fn now_unix() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
/// Spawns all watcher tasks. Returns the receiver end of the event channel.
/// `log_paths` comes from the loaded PatternFile.
pub fn spawn(log_paths: HashMap<String, String>) -> mpsc::Receiver<SystemEvent> {
let (tx, rx) = mpsc::channel::<SystemEvent>(256);
let tx_j = tx.clone();
tauri::async_runtime::spawn(async move {
journald::watch(tx_j).await;
});
let tx_k = tx.clone();
tauri::async_runtime::spawn(async move {
kmsg::watch(tx_k).await;
});
tauri::async_runtime::spawn(async move {
inotify::watch(log_paths, tx).await;
});
rx
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn event_source_can_be_cloned() {
let s = EventSource::AppLog {
app: "steam".into(),
};
let _ = s.clone();
assert!(matches!(s, EventSource::AppLog { .. }));
}
#[test]
fn system_event_constructed() {
let e = SystemEvent {
source: EventSource::Journald,
raw_line: "test line".into(),
timestamp: now_unix(),
};
assert_eq!(e.raw_line, "test line");
assert!(e.timestamp > 0);
}
}

View file

@ -38,6 +38,9 @@
"bundle": { "bundle": {
"active": true, "active": true,
"targets": ["deb", "rpm", "appimage"], "targets": ["deb", "rpm", "appimage"],
"resources": {
"patterns/*": "patterns/"
},
"icon": [ "icon": [
"icons/32x32.png", "icons/32x32.png",
"icons/128x128.png", "icons/128x128.png",
@ -45,8 +48,6 @@
"icons/icon.icns", "icons/icon.icns",
"icons/icon.ico" "icons/icon.ico"
], ],
"linux": { "linux": {}
"desktopTemplate": "../assets/robin.desktop"
}
} }
} }

View file

@ -33,13 +33,50 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, nextTick } from 'vue' import { ref, nextTick, onMounted, onUnmounted } from 'vue'
import { invoke } from '@tauri-apps/api/core'
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
interface Message { role: 'user' | 'robin'; content: string } interface Message { role: 'user' | 'robin'; content: string }
interface RobinEvent { pattern_id: string; title: string; body: string; severity: string; timestamp: number }
const messages = ref<Message[]>([]) const messages = ref<Message[]>([])
const input = ref('') const input = ref('')
const messagesEl = ref<HTMLElement | null>(null) const messagesEl = ref<HTMLElement | null>(null)
let unlisten: UnlistenFn | null = null
onMounted(async () => {
// Signal backend: stop queuing events into PENDING while panel is open.
// Must happen before drain so new events that arrive during mount are not
// double-delivered on the next open cycle.
try { await invoke('panel_opened') } catch {}
try {
const pending = await invoke<RobinEvent[]>('get_pending_events')
for (const e of pending) {
pushRobinEvent(e)
}
} catch (err) {
console.warn('Robin: failed to drain pending events:', err)
}
// Always set up the live listener, even if drain failed
unlisten = await listen<RobinEvent>('robin:event', ({ payload }) => {
pushRobinEvent(payload)
})
})
onUnmounted(() => {
unlisten?.()
invoke('panel_closed').catch(() => {})
})
function pushRobinEvent(e: RobinEvent) {
messages.value.push({ role: 'robin', content: `**${e.title}**\n\n${e.body}` })
nextTick(() => {
messagesEl.value?.scrollTo({ top: messagesEl.value.scrollHeight, behavior: 'smooth' })
})
}
async function send() { async function send() {
const text = input.value.trim() const text = input.value.trim()

View file

@ -2,9 +2,11 @@
<div class="onboarding"> <div class="onboarding">
<div class="onboarding-card"> <div class="onboarding-card">
<div class="robin-logo">🐦</div> <div class="robin-logo">🐦</div>
<!-- Step 1: Source OS -->
<template v-if="step === 1">
<h1>Hi, I'm Robin.</h1> <h1>Hi, I'm Robin.</h1>
<p>I help you find your feet on Linux. Before we start what were you using before?</p> <p>I help you find your feet on Linux. Before we start what were you using before?</p>
<div class="os-choices"> <div class="os-choices">
<button <button
v-for="os in osOptions" v-for="os in osOptions"
@ -16,46 +18,134 @@
{{ os.label }} {{ os.label }}
</button> </button>
</div> </div>
<p class="hint">Robin uses this to explain things in terms you already know.</p> <p class="hint">Robin uses this to explain things in terms you already know.</p>
<button class="continue-btn" :disabled="!selectedOs" @click="advanceFromStep1">
Continue
</button>
</template>
<!-- Step 2: Source Linux distro (Linux-to-Linux only) -->
<template v-else-if="step === 2 && selectedOs === 'linux'">
<h1>Which distro were you on?</h1>
<p>Robin loads the right patterns for your background.</p>
<div class="os-choices">
<button <button
class="continue-btn" v-for="d in linuxDistroOptions"
:disabled="!selectedOs" :key="d.value"
@click="submit" class="os-btn"
:class="{ selected: selectedSourceDistro === d.value }"
@click="selectedSourceDistro = d.value"
> >
{{ d.label }}
</button>
</div>
<button class="continue-btn" @click="advanceFromLinuxDistro">
{{ selectedSourceDistro ? 'Continue' : 'Skip' }}
</button>
</template>
<!-- Step 3: Dual-boot question (Windows / macOS only) -->
<template v-else-if="step === 3">
<h1>Are you keeping {{ sourceOsLabel }} alongside Linux?</h1>
<p>Dual-boot setups have some extra quirks Robin can watch for like Windows locking shared drives.</p>
<div class="os-choices">
<button
class="os-btn"
:class="{ selected: dualBoot === true }"
@click="dualBoot = true"
>
Yes, dual-booting
</button>
<button
class="os-btn"
:class="{ selected: dualBoot === false }"
@click="dualBoot = false"
>
No, Linux only
</button>
</div>
<button class="continue-btn" :disabled="dualBoot === null" @click="submit">
Let's go Let's go
</button> </button>
</template>
<!-- Final step for OS types that skip step 3 -->
<template v-else-if="step === 'submit'">
<h1>All set.</h1>
<p>Robin is ready to help.</p>
</template>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, computed } from 'vue'
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/core'
const emit = defineEmits<{ complete: [] }>() const emit = defineEmits<{ complete: [] }>()
type Step = 1 | 2 | 3 | 'submit'
const step = ref<Step>(1)
const selectedOs = ref('') const selectedOs = ref('')
const selectedSourceDistro = ref('')
const dualBoot = ref<boolean | null>(null)
const osOptions = [ const osOptions = [
{ value: 'windows', label: 'Windows' }, { value: 'windows', label: 'Windows' },
{ value: 'macos', label: 'macOS' }, { value: 'macos', label: 'macOS' },
{ value: 'linux', label: 'Another Linux distro' }, { value: 'linux', label: 'Another Linux distro' },
{ value: 'android', label: 'Android / ChromeOS' },
{ value: 'ipad', label: 'iPhone / iPad' },
] ]
async function submit() { const linuxDistroOptions = [
{ value: 'ubuntu', label: 'Debian / Ubuntu / Mint / Pop!_OS' },
{ value: 'fedora', label: 'Fedora / RHEL / CentOS' },
{ value: 'arch', label: 'Arch / Manjaro / EndeavourOS' },
{ value: 'opensuse', label: 'openSUSE' },
]
const sourceOsLabel = computed(() =>
osOptions.find(o => o.value === selectedOs.value)?.label ?? selectedOs.value
)
function advanceFromStep1() {
if (!selectedOs.value) return if (!selectedOs.value) return
if (selectedOs.value === 'linux') {
step.value = 2
} else if (selectedOs.value === 'windows' || selectedOs.value === 'macos') {
step.value = 3
} else {
// mobile users: no dual-boot question
submitOnboarding()
}
}
function advanceFromLinuxDistro() {
// Linux-to-Linux users don't get the dual-boot question
submitOnboarding()
}
async function submit() {
submitOnboarding()
}
async function submitOnboarding() {
const distro = await detectDistro() const distro = await detectDistro()
const sourceDistro = selectedSourceDistro.value || undefined
const dualBootWith = dualBoot.value === true ? selectedOs.value : undefined
await invoke('complete_onboarding', { await invoke('complete_onboarding', {
sourceOs: selectedOs.value, sourceOs: selectedOs.value,
distro, distro,
sourceDistro,
dualBootWith,
}) })
emit('complete') emit('complete')
} }
async function detectDistro(): Promise<string> { async function detectDistro(): Promise<string> {
// M1: read /etc/os-release via tauri-plugin-fs
// M0: return placeholder
return 'unknown' return 'unknown'
} }
</script> </script>
@ -70,7 +160,7 @@ async function detectDistro(): Promise<string> {
} }
.onboarding-card { .onboarding-card {
max-width: 320px; max-width: 340px;
text-align: center; text-align: center;
} }
@ -95,6 +185,7 @@ p { color: #aaa; line-height: 1.5; margin-bottom: 20px; }
color: #e0e0e0; color: #e0e0e0;
cursor: pointer; cursor: pointer;
transition: border-color 0.15s, background 0.15s; transition: border-color 0.15s, background 0.15s;
text-align: left;
} }
.os-btn:hover { border-color: #6c8ebf; } .os-btn:hover { border-color: #6c8ebf; }