feat: full pattern matrix — M1 complete, M2 LLM chat, 30+ pattern files #10
51 changed files with 11355 additions and 138 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -29,3 +29,6 @@ src-tauri/target/
|
||||||
# Secrets
|
# Secrets
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
|
|
||||||
|
# Visual companion brainstorm sessions
|
||||||
|
.superpowers/
|
||||||
|
|
|
||||||
283
manage.sh
Normal file → Executable file
283
manage.sh
Normal file → Executable file
|
|
@ -5,24 +5,144 @@ set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
APP_NAME="robin"
|
APP_NAME="robin"
|
||||||
|
RELEASE_BIN="$SCRIPT_DIR/src-tauri/target/release/$APP_NAME"
|
||||||
|
DEBUG_BIN="$SCRIPT_DIR/src-tauri/target/debug/$APP_NAME"
|
||||||
|
ICON="$SCRIPT_DIR/src-tauri/icons/128x128.png"
|
||||||
|
LOG_FILE="${XDG_DATA_HOME:-$HOME/.local/share}/tech.circuitforge.robin/logs/Robin.log"
|
||||||
|
|
||||||
cmd="${1:-help}"
|
cmd="${1:-help}"
|
||||||
|
|
||||||
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_find_binary() {
|
||||||
|
if [[ -f "$RELEASE_BIN" ]]; then
|
||||||
|
echo "$RELEASE_BIN"
|
||||||
|
elif [[ -f "$DEBUG_BIN" ]]; then
|
||||||
|
echo "$DEBUG_BIN"
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_require_binary() {
|
||||||
|
local bin
|
||||||
|
bin="$(_find_binary)"
|
||||||
|
if [[ -z "$bin" ]]; then
|
||||||
|
echo "Robin binary not found. Run one of:"
|
||||||
|
echo " ./manage.sh build (release, requires Node + Tauri CLI)"
|
||||||
|
echo " ./manage.sh build-debug (debug, requires Rust only)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "$bin"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Commands ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
case "$cmd" in
|
case "$cmd" in
|
||||||
|
|
||||||
|
run)
|
||||||
|
# Run the binary in the foreground (logs to terminal).
|
||||||
|
# Use this for manual testing; Ctrl+C to quit.
|
||||||
|
bin="$(_require_binary)"
|
||||||
|
echo "Starting Robin ($bin)..."
|
||||||
|
export DISPLAY="${DISPLAY:-:0}"
|
||||||
|
export RUST_LOG="${RUST_LOG:-robin_lib=info,warn}"
|
||||||
|
exec "$bin"
|
||||||
|
;;
|
||||||
|
|
||||||
|
start)
|
||||||
|
# Start Robin in the background (daemonised via nohup).
|
||||||
|
# Also starts the Vite dev server if a release binary is not available
|
||||||
|
# and Node/nvm is present, so the webview has something to connect to.
|
||||||
|
if pgrep -x "$APP_NAME" > /dev/null 2>&1; then
|
||||||
|
echo "Robin is already running (PID $(pgrep -x "$APP_NAME"))"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
bin="$(_require_binary)"
|
||||||
|
export DISPLAY="${DISPLAY:-:0}"
|
||||||
|
export RUST_LOG="${RUST_LOG:-robin_lib=info,warn}"
|
||||||
|
mkdir -p "$(dirname "$LOG_FILE")"
|
||||||
|
|
||||||
|
# If using the debug binary and no Vite server is running, start one.
|
||||||
|
if [[ "$bin" == *"target/debug"* ]] && ! curl -sf http://localhost:1420 > /dev/null 2>&1; then
|
||||||
|
NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
|
||||||
|
if [[ -s "$NVM_DIR/nvm.sh" ]] && command -v npm > /dev/null 2>&1 || { source "$NVM_DIR/nvm.sh" 2>/dev/null && command -v npm > /dev/null 2>&1; }; then
|
||||||
|
echo "Starting Vite dev server on :1420..."
|
||||||
|
nohup npm --prefix "$SCRIPT_DIR" run dev >> /tmp/robin-vite.log 2>&1 &
|
||||||
|
echo "Vite PID $! — logs: /tmp/robin-vite.log"
|
||||||
|
sleep 2 # give Vite time to bind before Robin connects
|
||||||
|
else
|
||||||
|
echo "Note: no release binary and Node not found — webview will show connection error."
|
||||||
|
echo "Run 'npm install && npm run dev' in $SCRIPT_DIR to fix this."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
nohup "$bin" >> "$LOG_FILE" 2>&1 &
|
||||||
|
echo "Robin started (PID $!). Logs: $LOG_FILE"
|
||||||
|
;;
|
||||||
|
|
||||||
|
stop)
|
||||||
|
if pgrep -x "$APP_NAME" > /dev/null 2>&1; then
|
||||||
|
pkill -x "$APP_NAME"
|
||||||
|
echo "Robin stopped."
|
||||||
|
else
|
||||||
|
echo "Robin is not running."
|
||||||
|
fi
|
||||||
|
# Stop Vite dev server if we started it.
|
||||||
|
if pgrep -f "vite" > /dev/null 2>&1; then
|
||||||
|
pkill -f "node.*vite" 2>/dev/null || true
|
||||||
|
echo "Vite dev server stopped."
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
restart)
|
||||||
|
"$0" stop || true
|
||||||
|
sleep 1
|
||||||
|
"$0" start
|
||||||
|
;;
|
||||||
|
|
||||||
|
status)
|
||||||
|
if pgrep -x "$APP_NAME" > /dev/null 2>&1; then
|
||||||
|
echo "Robin is running (PID $(pgrep -x "$APP_NAME"))"
|
||||||
|
else
|
||||||
|
echo "Robin is not running."
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
logs)
|
||||||
|
if [[ -f "$LOG_FILE" ]]; then
|
||||||
|
tail -f "$LOG_FILE"
|
||||||
|
else
|
||||||
|
echo "No log file yet at $LOG_FILE"
|
||||||
|
echo "Start Robin first: ./manage.sh start"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
dev)
|
dev)
|
||||||
echo "Starting Robin in dev mode..."
|
echo "Starting Robin in dev mode (hot-reload)..."
|
||||||
cd "$SCRIPT_DIR"
|
cd "$SCRIPT_DIR"
|
||||||
npm run tauri dev
|
npm run tauri dev
|
||||||
;;
|
;;
|
||||||
|
|
||||||
build)
|
build)
|
||||||
echo "Building Robin..."
|
echo "Building Robin release binary + installers..."
|
||||||
cd "$SCRIPT_DIR"
|
cd "$SCRIPT_DIR"
|
||||||
npm run tauri build
|
npm run tauri build
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
build-debug)
|
||||||
|
echo "Building Robin debug binary (Rust only, no Node needed)..."
|
||||||
|
cargo build --manifest-path "$SCRIPT_DIR/src-tauri/Cargo.toml"
|
||||||
|
echo "Binary: $DEBUG_BIN"
|
||||||
|
;;
|
||||||
|
|
||||||
|
test)
|
||||||
|
echo "Running Rust tests..."
|
||||||
|
cargo test --manifest-path "$SCRIPT_DIR/src-tauri/Cargo.toml" --lib
|
||||||
|
;;
|
||||||
|
|
||||||
install-deps)
|
install-deps)
|
||||||
echo "Installing system dependencies (Debian/Ubuntu)..."
|
echo "Installing system dependencies (Debian/Ubuntu/Mint)..."
|
||||||
sudo apt-get install -y \
|
sudo apt-get install -y \
|
||||||
libwebkit2gtk-4.1-dev \
|
libwebkit2gtk-4.1-dev \
|
||||||
libayatana-appindicator3-dev \
|
libayatana-appindicator3-dev \
|
||||||
|
|
@ -30,15 +150,12 @@ case "$cmd" in
|
||||||
libgtk-3-dev \
|
libgtk-3-dev \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
pkg-config
|
pkg-config
|
||||||
echo "Installing Node dependencies..."
|
|
||||||
cd "$SCRIPT_DIR"
|
|
||||||
npm install
|
|
||||||
echo "Installing Rust dependencies..."
|
echo "Installing Rust dependencies..."
|
||||||
cargo fetch --manifest-path "$SCRIPT_DIR/src-tauri/Cargo.toml"
|
cargo fetch --manifest-path "$SCRIPT_DIR/src-tauri/Cargo.toml"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
install-deps-arch)
|
install-deps-arch)
|
||||||
echo "Installing system dependencies (Arch/CachyOS)..."
|
echo "Installing system dependencies (Arch/Manjaro/CachyOS)..."
|
||||||
paru -S --needed \
|
paru -S --needed \
|
||||||
webkit2gtk-4.1 \
|
webkit2gtk-4.1 \
|
||||||
libayatana-appindicator \
|
libayatana-appindicator \
|
||||||
|
|
@ -46,72 +163,124 @@ case "$cmd" in
|
||||||
gtk3 \
|
gtk3 \
|
||||||
openssl \
|
openssl \
|
||||||
pkg-config
|
pkg-config
|
||||||
cd "$SCRIPT_DIR"
|
|
||||||
npm install
|
|
||||||
cargo fetch --manifest-path "$SCRIPT_DIR/src-tauri/Cargo.toml"
|
cargo fetch --manifest-path "$SCRIPT_DIR/src-tauri/Cargo.toml"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
install)
|
desktop-install)
|
||||||
echo "Installing Robin as a systemd user service..."
|
# Install .desktop entry so Robin appears in the system app menu.
|
||||||
SERVICE_DIR="$HOME/.config/systemd/user"
|
bin="$(_require_binary)"
|
||||||
mkdir -p "$SERVICE_DIR"
|
APPS_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
|
||||||
BINARY="$SCRIPT_DIR/src-tauri/target/release/$APP_NAME"
|
mkdir -p "$APPS_DIR"
|
||||||
if [[ ! -f "$BINARY" ]]; then
|
cat > "$APPS_DIR/robin.desktop" <<EOF
|
||||||
echo "Binary not found — run './manage.sh build' first"
|
[Desktop Entry]
|
||||||
exit 1
|
Version=1.0
|
||||||
fi
|
Type=Application
|
||||||
cat > "$SERVICE_DIR/robin.service" <<EOF
|
Name=Robin
|
||||||
[Unit]
|
GenericName=Linux Migration Assistant
|
||||||
Description=Robin — Linux migration companion
|
Comment=Helps you find your feet on Linux
|
||||||
After=graphical-session.target
|
Exec=$bin
|
||||||
|
Icon=$ICON
|
||||||
[Service]
|
Categories=Utility;System;
|
||||||
ExecStart=$BINARY
|
StartupNotify=false
|
||||||
Restart=on-failure
|
StartupWMClass=Robin
|
||||||
RestartSec=5
|
Keywords=linux;migration;help;assistant;
|
||||||
Environment=DISPLAY=:0
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=default.target
|
|
||||||
EOF
|
EOF
|
||||||
systemctl --user daemon-reload
|
update-desktop-database "$APPS_DIR" 2>/dev/null || true
|
||||||
systemctl --user enable robin
|
echo "Robin added to application menu."
|
||||||
echo "Robin installed. Start with: systemctl --user start robin"
|
echo "Entry: $APPS_DIR/robin.desktop"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
start)
|
desktop-remove)
|
||||||
systemctl --user start robin
|
DESKTOP="${XDG_DATA_HOME:-$HOME/.local/share}/applications/robin.desktop"
|
||||||
|
if [[ -f "$DESKTOP" ]]; then
|
||||||
|
rm "$DESKTOP"
|
||||||
|
update-desktop-database "$(dirname "$DESKTOP")" 2>/dev/null || true
|
||||||
|
echo "Robin removed from application menu."
|
||||||
|
else
|
||||||
|
echo "No desktop entry found at $DESKTOP"
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
stop)
|
autostart-enable)
|
||||||
systemctl --user stop robin
|
# Start Robin automatically when the desktop session begins.
|
||||||
|
bin="$(_require_binary)"
|
||||||
|
AUTOSTART_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/autostart"
|
||||||
|
mkdir -p "$AUTOSTART_DIR"
|
||||||
|
cat > "$AUTOSTART_DIR/robin.desktop" <<EOF
|
||||||
|
[Desktop Entry]
|
||||||
|
Version=1.0
|
||||||
|
Type=Application
|
||||||
|
Name=Robin
|
||||||
|
Comment=Helps you find your feet on Linux
|
||||||
|
Exec=$bin
|
||||||
|
Icon=$ICON
|
||||||
|
StartupNotify=false
|
||||||
|
X-GNOME-Autostart-enabled=true
|
||||||
|
Hidden=false
|
||||||
|
NoDisplay=false
|
||||||
|
EOF
|
||||||
|
echo "Robin will start automatically at login."
|
||||||
|
echo "Entry: $AUTOSTART_DIR/robin.desktop"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
status)
|
autostart-disable)
|
||||||
systemctl --user status robin
|
AUTOSTART="${XDG_CONFIG_HOME:-$HOME/.config}/autostart/robin.desktop"
|
||||||
|
if [[ -f "$AUTOSTART" ]]; then
|
||||||
|
rm "$AUTOSTART"
|
||||||
|
echo "Robin autostart disabled."
|
||||||
|
else
|
||||||
|
echo "No autostart entry found at $AUTOSTART"
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
logs)
|
install)
|
||||||
journalctl --user -u robin -f
|
# Full install: build-debug + desktop-install + autostart-enable
|
||||||
|
echo "=== Robin install ==="
|
||||||
|
"$0" build-debug
|
||||||
|
"$0" desktop-install
|
||||||
|
"$0" autostart-enable
|
||||||
|
echo ""
|
||||||
|
echo "Robin installed. It will start at next login."
|
||||||
|
echo "Start it now: ./manage.sh start"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
test)
|
uninstall)
|
||||||
cd "$SCRIPT_DIR"
|
"$0" stop || true
|
||||||
cargo test --manifest-path src-tauri/Cargo.toml
|
"$0" desktop-remove || true
|
||||||
|
"$0" autostart-disable || true
|
||||||
|
echo "Robin uninstalled."
|
||||||
;;
|
;;
|
||||||
|
|
||||||
help|*)
|
help|*)
|
||||||
echo "Usage: ./manage.sh <command>"
|
cat <<'EOF'
|
||||||
echo ""
|
Robin — Linux migration companion
|
||||||
echo " dev Start in development mode (hot-reload)"
|
Usage: ./manage.sh <command>
|
||||||
echo " build Build release binary + installers"
|
|
||||||
echo " install-deps Install system deps (Debian/Ubuntu)"
|
Running:
|
||||||
echo " install-deps-arch Install system deps (Arch/CachyOS)"
|
run Run in foreground (logs to terminal, Ctrl+C to quit)
|
||||||
echo " install Install as systemd user service"
|
start Start in background
|
||||||
echo " start Start Robin service"
|
stop Stop background instance
|
||||||
echo " stop Stop Robin service"
|
restart Stop then start
|
||||||
echo " status Show Robin service status"
|
status Show whether Robin is running
|
||||||
echo " logs Tail Robin logs"
|
logs Tail the Robin log file
|
||||||
echo " test Run Rust tests"
|
|
||||||
|
Building:
|
||||||
|
build-debug Build debug binary (Rust only, no Node/npm needed)
|
||||||
|
build Build release binary + .deb/.rpm/.AppImage (needs Node + Tauri CLI)
|
||||||
|
dev Start dev mode with hot-reload (needs Node + Tauri CLI)
|
||||||
|
test Run Rust unit tests
|
||||||
|
|
||||||
|
Installation:
|
||||||
|
install build-debug + desktop-install + autostart-enable
|
||||||
|
uninstall Remove desktop entry, autostart, and stop Robin
|
||||||
|
desktop-install Add Robin to the system application menu
|
||||||
|
desktop-remove Remove Robin from the application menu
|
||||||
|
autostart-enable Start Robin automatically at login
|
||||||
|
autostart-disable Stop Robin from starting at login
|
||||||
|
|
||||||
|
Dependencies:
|
||||||
|
install-deps Install system deps (Debian/Ubuntu/Mint)
|
||||||
|
install-deps-arch Install system deps (Arch/Manjaro/CachyOS)
|
||||||
|
EOF
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
|
||||||
11
package-lock.json
generated
11
package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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
6158
src-tauri/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
fn main() {
|
fn main() {
|
||||||
tauri_build::build()
|
tauri_build::build()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
136
src-tauri/patterns/android-to-arch.toml
Normal file
136
src-tauri/patterns/android-to-arch.toml
Normal 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"
|
||||||
137
src-tauri/patterns/android-to-debian.toml
Normal file
137
src-tauri/patterns/android-to-debian.toml
Normal 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."
|
||||||
102
src-tauri/patterns/android-to-fedora.toml
Normal file
102
src-tauri/patterns/android-to-fedora.toml
Normal 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"
|
||||||
103
src-tauri/patterns/android-to-opensuse.toml
Normal file
103
src-tauri/patterns/android-to-opensuse.toml
Normal 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."
|
||||||
146
src-tauri/patterns/arch-to-debian.toml
Normal file
146
src-tauri/patterns/arch-to-debian.toml
Normal 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."
|
||||||
136
src-tauri/patterns/arch-to-fedora.toml
Normal file
136
src-tauri/patterns/arch-to-fedora.toml
Normal 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)."
|
||||||
130
src-tauri/patterns/arch-to-opensuse.toml
Normal file
130
src-tauri/patterns/arch-to-opensuse.toml
Normal 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)."
|
||||||
186
src-tauri/patterns/debian-to-arch.toml
Normal file
186
src-tauri/patterns/debian-to-arch.toml
Normal 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"
|
||||||
144
src-tauri/patterns/debian-to-fedora.toml
Normal file
144
src-tauri/patterns/debian-to-fedora.toml
Normal 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)."
|
||||||
120
src-tauri/patterns/debian-to-opensuse.toml
Normal file
120
src-tauri/patterns/debian-to-opensuse.toml
Normal 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)."
|
||||||
65
src-tauri/patterns/dualboot-macos.toml
Normal file
65
src-tauri/patterns/dualboot-macos.toml
Normal 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)"
|
||||||
75
src-tauri/patterns/dualboot-windows.toml
Normal file
75
src-tauri/patterns/dualboot-windows.toml
Normal 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."
|
||||||
172
src-tauri/patterns/fedora-to-arch.toml
Normal file
172
src-tauri/patterns/fedora-to-arch.toml
Normal 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."
|
||||||
138
src-tauri/patterns/fedora-to-debian.toml
Normal file
138
src-tauri/patterns/fedora-to-debian.toml
Normal 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."
|
||||||
130
src-tauri/patterns/fedora-to-opensuse.toml
Normal file
130
src-tauri/patterns/fedora-to-opensuse.toml
Normal 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)."
|
||||||
120
src-tauri/patterns/ipad-to-arch.toml
Normal file
120
src-tauri/patterns/ipad-to-arch.toml
Normal 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"
|
||||||
137
src-tauri/patterns/ipad-to-debian.toml
Normal file
137
src-tauri/patterns/ipad-to-debian.toml
Normal 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."
|
||||||
102
src-tauri/patterns/ipad-to-fedora.toml
Normal file
102
src-tauri/patterns/ipad-to-fedora.toml
Normal 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"
|
||||||
111
src-tauri/patterns/ipad-to-opensuse.toml
Normal file
111
src-tauri/patterns/ipad-to-opensuse.toml
Normal 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."
|
||||||
219
src-tauri/patterns/linux-to-arch.toml
Normal file
219
src-tauri/patterns/linux-to-arch.toml
Normal 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."
|
||||||
177
src-tauri/patterns/macos-to-arch.toml
Normal file
177
src-tauri/patterns/macos-to-arch.toml
Normal 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."
|
||||||
172
src-tauri/patterns/opensuse-to-arch.toml
Normal file
172
src-tauri/patterns/opensuse-to-arch.toml
Normal 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."
|
||||||
138
src-tauri/patterns/opensuse-to-debian.toml
Normal file
138
src-tauri/patterns/opensuse-to-debian.toml
Normal 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."
|
||||||
146
src-tauri/patterns/opensuse-to-fedora.toml
Normal file
146
src-tauri/patterns/opensuse-to-fedora.toml
Normal 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)."
|
||||||
187
src-tauri/patterns/windows-to-debian.toml
Normal file
187
src-tauri/patterns/windows-to-debian.toml
Normal 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"
|
||||||
|
|
@ -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(())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>,
|
||||||
/// 0–5: grows as user dismisses suggestions they already know
|
/// 0–5: 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
88
src-tauri/src/distro.rs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,14 +26,106 @@ 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();
|
||||||
|
|
||||||
|
// Intercept the chat window's close button so it hides rather than
|
||||||
|
// destroys the window. Without this, closing once makes the window
|
||||||
|
// unreachable — get_webview_window("chat") returns None and the tray
|
||||||
|
// click does nothing.
|
||||||
|
if let Some(chat_win) = app.get_webview_window("chat") {
|
||||||
|
let win = chat_win.clone();
|
||||||
|
chat_win.on_window_event(move |event| {
|
||||||
|
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||||
|
let _ = win.hide();
|
||||||
|
api.prevent_close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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!())
|
.build(tauri::generate_context!())
|
||||||
.expect("error while running Robin");
|
.expect("error building Robin")
|
||||||
|
.run(|_app, event| {
|
||||||
|
// Robin lives in the system tray. Prevent the process from exiting
|
||||||
|
// when the chat window is closed or hidden — only the tray "Quit"
|
||||||
|
// menu item should terminate the app.
|
||||||
|
if let tauri::RunEvent::ExitRequested { api, .. } = event {
|
||||||
|
api.prevent_exit();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
157
src-tauri/src/llm.rs
Normal file
157
src-tauri/src/llm.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
95
src-tauri/src/notify.rs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
347
src-tauri/src/patterns.rs
Normal file
347
src-tauri/src/patterns.rs
Normal file
|
|
@ -0,0 +1,347 @@
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Helper: push paths for a given filename into candidates.
|
||||||
|
let mut push_candidates = |filename: &str| {
|
||||||
|
// 1. Relative to binary: covers both bundled installs (patterns/ next to binary)
|
||||||
|
// and dev builds (target/debug/ → ../../patterns/ = src-tauri/patterns/).
|
||||||
|
if let Ok(exe) = std::env::current_exe() {
|
||||||
|
if let Some(exe_dir) = exe.parent() {
|
||||||
|
candidates.push(exe_dir.join("patterns").join(filename).display().to_string());
|
||||||
|
candidates.push(exe_dir.join("../../patterns").join(filename).display().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 2. CWD-relative fallbacks (for manual / script invocations).
|
||||||
|
candidates.push(format!("patterns/{filename}"));
|
||||||
|
candidates.push(format!("src-tauri/patterns/{filename}"));
|
||||||
|
// 3. System install path.
|
||||||
|
candidates.push(format!("/usr/share/robin/patterns/{filename}"));
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(src_distro) = source_distro_family {
|
||||||
|
push_candidates(&format!("{src_distro}-to-{distro_family}.toml"));
|
||||||
|
}
|
||||||
|
let generic = format!("{source_os}-to-{distro_family}.toml");
|
||||||
|
push_candidates(&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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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");
|
|
||||||
}
|
|
||||||
124
src-tauri/src/watcher/inotify.rs
Normal file
124
src-tauri/src/watcher/inotify.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
74
src-tauri/src/watcher/journald.rs
Normal file
74
src-tauri/src/watcher/journald.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
68
src-tauri/src/watcher/kmsg.rs
Normal file
68
src-tauri/src/watcher/kmsg.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
85
src-tauri/src/watcher/mod.rs
Normal file
85
src-tauri/src/watcher/mod.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -2,60 +2,150 @@
|
||||||
<div class="onboarding">
|
<div class="onboarding">
|
||||||
<div class="onboarding-card">
|
<div class="onboarding-card">
|
||||||
<div class="robin-logo">🐦</div>
|
<div class="robin-logo">🐦</div>
|
||||||
<h1>Hi, I'm Robin.</h1>
|
|
||||||
<p>I help you find your feet on Linux. Before we start — what were you using before?</p>
|
|
||||||
|
|
||||||
<div class="os-choices">
|
<!-- Step 1: Source OS -->
|
||||||
<button
|
<template v-if="step === 1">
|
||||||
v-for="os in osOptions"
|
<h1>Hi, I'm Robin.</h1>
|
||||||
:key="os.value"
|
<p>I help you find your feet on Linux. Before we start — what were you using before?</p>
|
||||||
class="os-btn"
|
<div class="os-choices">
|
||||||
:class="{ selected: selectedOs === os.value }"
|
<button
|
||||||
@click="selectedOs = os.value"
|
v-for="os in osOptions"
|
||||||
>
|
:key="os.value"
|
||||||
{{ os.label }}
|
class="os-btn"
|
||||||
|
:class="{ selected: selectedOs === os.value }"
|
||||||
|
@click="selectedOs = os.value"
|
||||||
|
>
|
||||||
|
{{ os.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<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>
|
</button>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
<p class="hint">Robin uses this to explain things in terms you already know.</p>
|
<!-- 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
|
||||||
|
v-for="d in linuxDistroOptions"
|
||||||
|
:key="d.value"
|
||||||
|
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>
|
||||||
|
|
||||||
<button
|
<!-- Step 3: Dual-boot question (Windows / macOS only) -->
|
||||||
class="continue-btn"
|
<template v-else-if="step === 3">
|
||||||
:disabled="!selectedOs"
|
<h1>Are you keeping {{ sourceOsLabel }} alongside Linux?</h1>
|
||||||
@click="submit"
|
<p>Dual-boot setups have some extra quirks Robin can watch for — like Windows locking shared drives.</p>
|
||||||
>
|
<div class="os-choices">
|
||||||
Let's go
|
<button
|
||||||
</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
|
||||||
|
</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; }
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,9 @@ import vue from '@vitejs/plugin-vue'
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
|
server: {
|
||||||
|
port: 1420,
|
||||||
|
strictPort: true, // fail loudly if 1420 is taken rather than silently shifting
|
||||||
|
},
|
||||||
|
clearScreen: false, // keep terminal output readable alongside cargo/Tauri logs
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue