Compare commits

..
19 Commits
Author SHA1 Message Date
mw cd63df4f59 docs(caddy): add agent0.localhost reverse proxy + RFC 6761 wildcard learning
- New site: https://agent0.localhost -> reverse_proxy 192.168.0.141:8090
  (first non-localhost backend; remote LAN host running uvicorn)
- Config in dedicated /etc/caddy/conf.d/agent0.localhost.conf
- Regenerated mkcert cert: 19 -> 20 SANs, valid until 2028-10-21
- Key learning: *.localhost wildcard is NOT honored by curl/system
  trust even for single-label subdomains - .localhost is a special-use
  TLD per RFC 6761; every subdomain needs an explicit SAN. Corrects
  prior assumption in stack.md and manual-overrides.md.

Updates: docs/tech/stack.md, docs/tech/manual-overrides.md, CHANGELOG.md
2026-07-21 14:08:07 +02:00
mw c67dd74e0b docs: add memory inventory and migration log
- 4x 8 GiB DDR4-3000 DIMMs (32 GiB total) in workstation
- 64 GiB DDR4-2400 migrated to homeoffice, swapped for 32 GiB
2026-07-13 17:29:00 +02:00
mw cbe24c03df docs: add platform/packages/onlyoffice.docs localhost sites + cert regen to Caddy docs
- Added platform.localhost (-> localhost:10080) and packages.localhost
  (-> localhost:20080) to stack.md sites table (total now 17)
- Documented onlyoffice.docs.localhost (pre-existing in Caddy config, was undocumented)
- Regenerated mkcert cert with explicit SANs for the 3 new hostnames
  (onlyoffice.docs.localhost is multi-label, not covered by *.localhost wildcard)
- Fixed wildcard note: multi-label hosts require explicit SANs
- Updated manual-overrides with cert regeneration row + site delta
- Updated CHANGELOG with cert regen + site additions
2026-07-12 13:10:21 +02:00
mwandJunie 269079df34 chore(hyprland): enable mouse window drag/resize and migrate cheatsheets to learn portal
Co-authored-by: Junie <junie@jetbrains.com>
2026-07-09 11:01:51 +02:00
mw b888ef90c4 docs(changelog): merge duplicate '### Fixed' section into one block
Complete the Keep a Changelog compliance started in 441e28b. The second
'### Fixed' block at end of [Unreleased] (kitty, GRUB regex,
changed_when, OOM, keyring) is merged into the first block, preserving
newest-first ordering (2026-05-22 -> 2026-03-17).

Section order remains Fixed -> Added -> Removed -> Changed (pre-existing,
non-standard vs Keep a Changelog's recommended Added-first order).
Reordering all sections is a larger refactor beyond this fix's scope.
2026-07-06 09:33:56 +02:00
mw 441e28bda3 docs: fix CHANGELOG duplicate Added section + missing period in learning doc
Address review feedback on ce99f58:
- Merge the two '### Added' blocks in [Unreleased] into one (Keep a
  Changelog: each category appears once). New 2026-07-06 entries now
  sit at the top of the existing Added block.
- Add missing period after 'required SSE headers' in the liveness
  verification prose (was a run-on sentence).
2026-07-06 09:14:13 +02:00
mw ce99f58374 docs: Caddy manual override + OnlyOffice MCP SSE investigation (#92)
Record the Caddy reverse-proxy at /etc/caddy/ (14 *.localhost sites,
mkcert TLS, systemd caddy.service) as a manual override in
docs/tech/manual-overrides.md - no caddy Ansible role exists yet.

Add a 'Local Dev Services' section to docs/tech/stack.md listing all
*.localhost sites and useful Caddy commands.

New learning record docs/learnings/2026-07-06-caddy-onlyoffice-mcp-sse-
investigation.md: Caddy emits 'context canceled' SSE aborts (~1.5ms) on
onlyoffice.localhost/mcp -> localhost:3847. Upstream verified alive
(HTTP 406 to plain GET = expected). Handed off to GitLab issue
satware/mcp/onlyoffice#92 with 5 hypotheses, 7 tasks, and a proposed
flush_interval -1 + transport http { versions 1.1 } mitigation (not
applied yet to avoid masking root cause).
2026-07-06 08:58:27 +02:00
mw 2b6ce0d635 docs: Remmina Wayland crash fix + upstream MR !2757
- Added learning doc for 2026-07-03 session: diagnosed SIGSEGV in
  XKeysymToKeycode (missing NULL check on XOpenDisplay), forked Remmina
  to gitlab.com/satware, built with FreeRDP3, verified fix, submitted MR.
- Added manual override entry for patched remmina-plugin-rdp.so in
  ~/.config/remmina/plugins/
2026-07-03 12:13:34 +02:00
mw d72e3782e8 docs(passwords): document git.satware.ai PAT libsecret migration and rotation TODO 2026-06-30 14:13:15 +02:00
mw 07bd3f8699 fix(ansible): harden AUR install task and rename ansible_user to local_user
- AUR task: replace loop with single paru call, use changed_when: false
  (stdout-only match never fired under non-TTY runs) and failed_when: false
  so transient AUR failures don't abort the play
- Rename ansible_user -> local_user to avoid shadowing the connection
  magic var; update the two references in roles/common/tasks/main.yml
2026-06-24 20:14:33 +02:00
mw feaccf201d docs(learnings): record YubiKey + Dell E7250 air-gapped keygen host setup
Session record 2026-06-24: YubiKey inventory, GPG commit-signing plan,
reusable SPECCOL inventory/provisioning stick (dock-aware), and full
hardening + provisioning of the Dell Latitude E7250 as the dedicated
air-gapped keygen/forensic host (BIOS A24, Secure Boot, AMT off, radios
off, dist-upgraded, Phase-2 tooling installed). TPM 1.2-only (hardware
limit). Captures key gotchas (yubikey-manager package name, FAT32 +x,
TXT-before-TPM-clear, E7250 not on LVFS) and the refined architecture.
2026-06-24 20:02:43 +02:00
mw 5a714e6256 docs(plans): add YubiKey+GPG GitHub commit-signing plan
Baby-step plan for hardware-backed OpenPGP commit/tag signing on YubiKey 5
NFC #30836069 (air-gapped keygen, ed25519/cv25519 subkeys, 2y expiry, touch
required). LUKS-encrypted recovery backup on the Type-C USB stick; non-secret
config to be folded into ansible/. HTTPS+GPG-sign (lowest risk); 2nd YubiKey
scheduled as Phase 9 follow-up.
2026-06-24 16:54:08 +02:00
ja d9392447f9 doxify: inject DOX framework into AGENTS.md and update system config docs
- Inject all 10 DOX sections into root AGENTS.md (Core Contract, Read Before
  Editing, Update After Editing, Hierarchy, Child Doc Shape, Style, Closeout,
  User Preferences, Child DOX Index)
- Update ADR 0002: linux612->linux618, 575xx->580xx NVIDIA migration
- Update setup.md: kernel/driver refs to current linux618 + 580xx
- Update stack.md, hardware-inventory.md: NVIDIA note 595.71->580xx
- Update manual-overrides.md: add br_vm_internal bridge deletion entry
- Update workstation-health.md: add Recent Cleanup section
- Update README.md: current software versions, expand roles list
- Add docs/neovim-cheatsheet.md
- Bump last_updated to 2026-06-17 on all modified docs
2026-06-17 14:13:34 +02:00
jaandJunie de24990208 chore: add maintenance scripts and update editor preference
- Add scripts/cleanup.sh for package cleanup (dry-run/apply modes)
- Add scripts/nvidia-recovery.sh for broken GUI recovery
- Update AGENTS.md with new script references
- Change default editor from vim to nvim in bashrc

Co-authored-by: Junie <junie@jetbrains.com>
2026-06-15 06:43:43 +02:00
jaandJunie fe7f133136 feat(hyprland): migrate config from .conf to .lua format (Hyprland 0.55+)
- Add hyprland.lua.j2 Ansible template with full Lua DSL config
- Update tasks to deploy hyprland.lua instead of hyprland.conf
- Add docs/tech/hyprland-lua-migration.md with API reference,
  dispatcher mapping, pitfalls, and validation commands
- Update keybindings doc to reflect .lua config format

Co-authored-by: Junie <junie@jetbrains.com>
2026-06-15 06:43:23 +02:00
ja f1544747a0 fix: replace numlockx with native numlock_by_default; remove invalid --numlock from greetd
- Remove invalid --numlock flag from tuigreet command in greetd config
  (not a recognized tuigreet parameter, caused boot error)
- Replace X11-only 'numlockx on' in Hyprland with native
  'numlock_by_default = true' in the input section
- Update Ansible template and CHANGELOG accordingly
2026-06-01 06:54:39 +02:00
ja cb9b8474bb Add kitty window title bar with hostname/cwd display
- New Ansible template for kitty.conf with window_title_bar
- Title bar shows: hostname | cwd | title on every tile
- Document title bar feature in keybindings cheatsheet
2026-05-28 10:43:40 +02:00
ja 3c7bf3bd73 Add SUPER+Enter as alternative terminal shortcut 2026-05-28 08:54:33 +02:00
ja 5173fdd5e0 Enable NumLock on boot via tuigreet and Hyprland
- Install numlockx
- Add --numlock flag to tuigreet greeter
- Add exec-once = numlockx on to Hyprland config
2026-05-28 08:32:40 +02:00
29 changed files with 2001 additions and 442 deletions
+85
View File
@@ -14,6 +14,10 @@ This repository manages the infrastructure, configuration, and maintenance for M
|------|---------|
| Apply Configuration | `ansible-playbook ansible/workstation.yml` |
| System Maintenance | `bash scripts/maintenance.sh` |
| Package Cleanup (dry-run) | `bash scripts/cleanup.sh --check` |
| Package Cleanup (apply) | `bash scripts/cleanup.sh --apply` |
| NVIDIA Recovery (broken GUI) | `bash scripts/nvidia-recovery.sh --apply` |
| **Post-recovery** | **Reboot required to restore GUI login** |
| Verify Ansible | `ansible-playbook ansible/workstation.yml --check` |
## Standards
@@ -31,3 +35,84 @@ This repository manages the infrastructure, configuration, and maintenance for M
- Global rules: `Rules/`
- Project rules: `.clinerules/`
- Agent instructions: `AGENTS.md` (this file)
# DOX Framework
## Core Contract
- AGENTS.md files are binding work contracts for their subtrees
- Work products, source materials, instructions, records, assets, and durable docs must stay understandable from the nearest applicable AGENTS.md plus every parent AGENTS.md above it
## Read Before Editing
1. Read the root AGENTS.md
2. Identify every file or folder you expect to touch
3. Walk from the repository root to each target path
4. Read every AGENTS.md found along each route
5. If a parent AGENTS.md lists a child AGENTS.md whose scope contains the path, read that child and continue from there
6. Use the nearest AGENTS.md as the local contract and parent docs for repo-wide rules
7. If docs conflict, the closer doc controls local work details, but no child doc may weaken DOX
Do not rely on memory. Re-read the applicable DOX chain in the current session before editing.
## Update After Editing
Every meaningful change requires a DOX pass before the task is done.
Update the closest owning AGENTS.md when a change affects:
- purpose, scope, ownership, or responsibilities
- durable structure, contracts, workflows, or operating rules
- required inputs, outputs, permissions, constraints, side effects, or artifacts
- user preferences about behavior, communication, process, organization, or quality
- AGENTS.md creation, deletion, move, rename, or index contents
Update parent docs when parent-level structure, ownership, workflow, or child index changes. Update child docs when parent changes alter local rules. Remove stale or contradictory text immediately. Small edits that do not change behavior or contracts may leave docs unchanged, but the DOX pass still must happen.
## Hierarchy
- Root AGENTS.md is the DOX rail: project-wide instructions, global preferences, durable workflow rules, and the top-level Child DOX Index
- Child AGENTS.md files own domain-specific instructions and their own Child DOX Index
- Each parent explains what its direct children cover and what stays owned by the parent
- The closer a doc is to the work, the more specific and practical it must be
## Child Doc Shape
- Create a child AGENTS.md when a folder becomes a durable boundary with its own purpose, rules, responsibilities, workflow, materials, or quality standards
- Work Guidance must reflect the current standards of the project or user instructions; if there are no specific standards or instructions yet, leave it empty
- Verification must reflect an existing check; if no verification framework exists yet, leave it empty and update it when one exists
Default section order:
- Purpose
- Ownership
- Local Contracts
- Work Guidance
- Verification
- Child DOX Index
## Style
- Keep docs concise, current, and operational
- Document stable contracts, not diary entries
- Put broad rules in parent docs and concrete details in child docs
- Prefer direct bullets with explicit names
- Do not duplicate rules across many files unless each scope needs a local version
- Delete stale notes instead of explaining history
- Trim obvious statements, repeated rules, misplaced detail, and warnings for risks that no longer exist
## Closeout
1. Re-check changed paths against the DOX chain
2. Update nearest owning docs and any affected parents or children
3. Refresh every affected Child DOX Index
4. Remove stale or contradictory text
5. Run existing verification when relevant
6. Report any docs intentionally left unchanged and why
## User Preferences
When the user requests a durable behavior change, record it here or in the relevant child AGENTS.md
## Child DOX Index
No child AGENTS.md files found. The root AGENTS.md covers the entire project scope.
+14 -6
View File
@@ -8,9 +8,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Fixed
- Fixed Hyprland configuration warning/error by changing the DVI-D-1 monitor `transform` property from a string `"1"` to an integer `1` (2026-07-09).
- tuigreet boot error: removed invalid `--numlock` flag from greetd config (not a valid tuigreet parameter). Replaced X11-only `numlockx on` in Hyprland with native `numlock_by_default = true` in the `input` section (2026-06-01).
- hypridle SEGV crash loop (ADR N/A, hotfix): sdbus-cpp 2.3.0 had an ABI incompatibility causing hypridle to SIGSEGV in `libsdbus-c++.so.2` `registerSignalHandler`. Fixed by upgrading sdbus-cpp from 2.3.0-1 to 2.3.1-1 (Arch upstream, pulled ahead of Manjaro mirror sync). Also resolves latent ABI issues for hyprlock and xdg-desktop-portal-hyprland (2026-05-22).
- Resolved kitty cursor keys not working by disabling the `TERM=xterm-256color` override in `.bashrc`, allowing Kitty to correctly set `TERM=xterm-kitty`. Added Ansible management for `kitty.conf` to ensure proper terminal behavior and shell integration (2026-05-19).
- `system-upgrade` role: GRUB menu assertion now matches Manjaro's real `menuentry 'Manjaro Linux (Kernel: X.Y.Z-MANJARO x64)'` format (counted per-kernel-family) instead of the wrong `menuentry .*linux(612|70)` regex that always returned 0 hits on Manjaro and broke the live run with a false-negative failure (2026-05-18).
- `system-upgrade` role: migration task `changed_when` now matches pacman's progress lines `^<!--(SECTION)-->(N/M)<!--(--SECTION)-->s+(installing|upgrading|removing)s` instead of plain substring matching, so the role's own echo lines no longer trigger spurious `changed=true` on idempotent re-runs. Verified: second live re-run reports `ok=16 changed=0 failed=0 skipped=5` (2026-05-18).
- Resolved system unresponsiveness issue by addressing critical memory/swap pressure with increased headroom and proactive OOM killing (2026-03-17).
- Identified failed `archlinux-keyring-wkd-sync.service` in system health check.
### Added
- Added `agent0.localhost` (-> `reverse_proxy 192.168.0.141:8090`, `flush_interval -1`) to Caddy in a dedicated file `/etc/caddy/conf.d/agent0.localhost.conf`. First non-localhost backend (remote LAN host running a uvicorn app). Regenerated mkcert cert to add explicit `agent0.localhost` SAN; cert now has 20 SANs, valid until 2028-10-21 (2026-07-21). Key learning: the `*.localhost` wildcard is NOT honored by curl/system trust even for single-label subdomains - `.localhost` is a special-use TLD per RFC 6761, so every subdomain needs an explicit SAN. This corrects the prior assumption in `docs/tech/stack.md` and `docs/tech/manual-overrides.md` that single-label subdomains were wildcard-covered. Total `*.localhost` sites now 18.
- Regenerated mkcert cert at `/etc/caddy/localhost{,-key}.pem` to add explicit SANs for `platform.localhost`, `packages.localhost`, and `onlyoffice.docs.localhost` (pre-existing in Caddy config but missing from cert, causing TLS validation failures). Cert now has 19 SANs, valid until 2028-10-12 (2026-07-12).
- Added `platform.localhost` (-> `localhost:10080`, amicron-platform Docker) and `packages.localhost` (-> `localhost:20080`, satis Docker) to `/etc/caddy/conf.d/localhost-dev.conf`. Docker port mappings updated: `~/internal/amicron-platform/docker-compose.override.yml` `10080:80`, `~/internal/satis/docker-compose.yml` nginx `20080:80`. Both upstreams plain HTTP on high ports; Caddy terminates TLS. Also documented `onlyoffice.docs.localhost` in `docs/tech/stack.md` (pre-existing in Caddy config, was undocumented). Total `*.localhost` sites now 17. The three form the local E2E test triangle: `platform.localhost` (ERP) -> `packages.localhost` (Satis) -> shared Firebird 3.0.14 database (token validation), mirroring the production `platform.auc.de` + `packages.auc.de` + shared Firebird setup (2026-07-12).
- Enabled click-and-drag window movement and resizing under the SUPER modifier, and enabled border dragging to resize tiled windows natively, updating both the Hyprland configuration template and keybindings cheatsheet (2026-07-09).
- Documented local dev services in `docs/tech/stack.md`: Caddy (systemd `caddy.service` on port 443) with 14 `*.localhost` sites and mkcert TLS at `/etc/caddy/`. Registered as a manual override in `docs/tech/manual-overrides.md` (no `caddy` Ansible role exists yet) (2026-07-06).
- Investigation learning record `docs/learnings/2026-07-06-caddy-onlyoffice-mcp-sse-investigation.md`: Caddy `context canceled` SSE aborts (~1.5ms) on `onlyoffice.localhost/mcp` forwarded to `localhost:3847` (OnlyOffice MCP server). Upstream verified alive (HTTP 406 to plain GET = expected). Handed off to GitLab issue [satware/mcp/onlyoffice#92](https://gitlab.satware.com/satware/mcp/onlyoffice/-/work_items/92) with 5 hypotheses, 7 investigation tasks, and a proposed Caddy `flush_interval -1` + `transport http { versions 1.1 }` mitigation (not applied yet) (2026-07-06).
- Waybar USB module: auto-mount USB sticks via udisks2, show hover details (device/model/size/fs/mountpoint), left-click opens in yazi, right-click ejects. Script at `~/.config/waybar/scripts/usb.sh`, Ansible-managed via `hyprland` role template `waybar_usb_script.sh.j2` (2026-05-22).
- `udisks2` explicitly listed in `hyprland_packages` (was transitive dep of Dolphin; now direct dep of USB module) (2026-05-22).
- Yazi optional deps provisioned via Ansible: `chafa`, `7zip`, `resvg` added to `hyprland_packages` (2026-05-22).
@@ -29,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Comprehensive health report for `mw-pfeddersheim-workstation` (2026-03-09).
### Removed
- Moved workstation-specific cheatsheets (`hyprland-keybindings.md`, `screenshot-cheatsheet.md`, `yazi-cheatsheet.md`, and the obsolete workstation copy of `neovim-cheatsheet.md`) to the central `learn` repository (2026-07-09).
- `alesi-routine-morning.service` / `alesi-routine-morning.timer` user-level systemd units — daily 07:45 morning routine was failing (exit 128, git outside repo). Script `scripts/daily-routine.sh` retained for manual use (2026-05-26).
- Tier 1 package cleanup (ADR 0007): removed 25 X11/KDE/legacy packages via Ansible — lightdm, lightdm-slick-greeter, lightdm-settings, sddm, sddm-kcm, sddm-breath-theme, xf86-video-{amdgpu,ati,intel,nouveau,vesa}, xf86-input-synaptics, xterm, xorg-{twm,xinit,xkill,server-xvfb}, x11vnc, numlockx, startup-notification, illyria-wallpaper, ntp, spectacle, pamac-tray-icon-plasma. All have Wayland-native replacements already in use. Frees ~80-120 MiB direct + cascade deps (2026-05-22).
- Tier 2 package cleanup (ADR 0007): removed KDE Plasma stack — kwin, plasma-desktop, plasma-workspace, kdeplasma-addons, plasma-nm, plasma-pa, plasma-x11-session, kde-gtk-config, powerdevil, xdg-desktop-portal-kde, plasma-integration, dolphin, dolphin-plugins, kdegraphics-thumbnailers. Plus 45 cascade orphan deps. Dolphin replaced by yazi. Frees ~400 MiB total (2026-05-22).
@@ -58,9 +72,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Cleaned up deprecated software information from `docs/tech/stack.md` (2026-03-13).
- Synchronized software versions across documentation files (2026-03-13).
### Fixed
- Resolved kitty cursor keys not working by disabling the `TERM=xterm-256color` override in `.bashrc`, allowing Kitty to correctly set `TERM=xterm-kitty`. Added Ansible management for `kitty.conf` to ensure proper terminal behavior and shell integration (2026-05-19).
- `system-upgrade` role: GRUB menu assertion now matches Manjaro's real `menuentry 'Manjaro Linux (Kernel: X.Y.Z-MANJARO x64)'` format (counted per-kernel-family) instead of the wrong `menuentry .*linux(612|70)` regex that always returned 0 hits on Manjaro and broke the live run with a false-negative failure (2026-05-18).
- `system-upgrade` role: migration task `changed_when` now matches pacman's progress lines `^<!--(SECTION)-->(N/M)<!--(--SECTION)-->s+(installing|upgrading|removing)s` instead of plain substring matching, so the role's own echo lines no longer trigger spurious `changed=true` on idempotent re-runs. Verified: second live re-run reports `ok=16 changed=0 failed=0 skipped=5` (2026-05-18).
- Resolved system unresponsiveness issue by addressing critical memory/swap pressure with increased headroom and proactive OOM killing (2026-03-17).
- Identified failed `archlinux-keyring-wkd-sync.service` in system health check.
+7 -10
View File
@@ -8,7 +8,7 @@ tags:
- infrastructure-as-code
- linux
- automation
last_updated: '2026-03-17'
last_updated: '2026-06-17'
---
# saTway® Infrastructure - mw-pfeddersheim-workstation
@@ -34,25 +34,22 @@ bash scripts/daily-routine.sh morning-full
bash scripts/daily-routine.sh eod
```
## 📊 Current Status (2026-03-17)
- **System Health**: ✅ Healthy (Load: 0.40, Disk: 84% used, Memory: 19% used, Swap: 16GB)
- **Active Containers**: 6 running (with memory limits)
- **Network**: Tailscale active
## 📂 Project Structure
| Directory | Purpose |
|-----------|---------|
| `ansible/` | Configuration-as-Code (Roles: `common`, `dev-tools`, `maintenance`) |
| `ansible/` | Configuration-as-Code (Roles: `system-upgrade`, `common`, `dev-tools`, `maintenance`, `fonts`, `pdf`, `hyprland`, `greetd`) |
| `config/` | System configuration templates (`.j2`) |
| `docs/` | PARA-structured documentation (`product`, `adr`, `learnings`, `tech`) |
| `docs/` | PARA-structured documentation (`product`, `adr`, `learnings`, `tech`, `workflows`, `plans`) |
| `scripts/` | Maintenance and automation scripts |
## 🛠 Software Stack
- **Languages**: Node.js 22 LTS, Python 3.12+, PHP 8.4
- **Languages**: Node.js 25.6.1, Python 3.14.3, PHP 8.5.3 (CLI)
- **Automation**: Ansible, Docker
- **Desktop**: Hyprland (Wayland), greetd + tuigreet, Waybar, Kitty
- **Kernel**: linux618 (LTS fallback) + linux70 (daily driver)
- **NVIDIA**: 580xx branch (Pascal/GT 1050 Ti)
- **Environment**: Manjaro/Arch Linux, Bash
## 📜 Core Principles
+20
View File
@@ -1,4 +1,24 @@
---
- name: Ensure kitty config directory exists
file:
path: "{{ ansible_env.HOME }}/.config/kitty"
state: directory
owner: "{{ local_user }}"
group: "{{ local_user }}"
mode: '0755'
tags: [kitty, terminal]
- name: Deploy kitty configuration
template:
src: kitty.conf.j2
dest: "{{ ansible_env.HOME }}/.config/kitty/kitty.conf"
owner: "{{ local_user }}"
group: "{{ local_user }}"
mode: '0644'
vars:
kitty_config_dir: "{{ ansible_env.HOME }}/.config/kitty"
tags: [kitty, terminal]
- name: Check current swap file size
stat:
path: "{{ swap_file_path }}"
@@ -0,0 +1,17 @@
# Managed by Ansible - saTway DevOps
shell bash
editor vim
# Shell integration is enabled by default in kitty 0.24.0+
# but we can explicitly enable it for clarity.
shell_integration enabled
# Ensure cursor keys work correctly by not overriding TERM in shell
# (Handled via .bashrc fix)
# Window title bar — show hostname + cwd on every tile
window_title_bar top
window_title_bar_min_windows 1
window_title_template "{hostname} | {cwd} | {title}"
include {{ kitty_config_dir }}/kitty-cline-keybindings.conf
+8
View File
@@ -86,6 +86,14 @@
changed_when: false
tags: [python, pipx]
- name: Ensure JetBrains Toolbox and IntelliJ IDEA Ultimate are installed from AUR
become: true
become_user: "{{ system_user }}"
command: paru -S --noconfirm --needed jetbrains-toolbox intellij-idea-ultimate-edition
changed_when: false
failed_when: false
tags: [jetbrains, aur]
- name: Ensure redundant and non-core software is removed
community.general.pacman:
name:
+1 -1
View File
@@ -88,7 +88,7 @@
group: "{{ system_user }}"
mode: '0644'
loop:
- { src: 'hyprland.conf.j2', dest: 'hypr/hyprland.conf' }
- { src: 'hyprland.lua.j2', dest: 'hypr/hyprland.lua' }
- { src: 'hyprlock.conf.j2', dest: 'hypr/hyprlock.conf' }
- { src: 'hypridle.conf.j2', dest: 'hypr/hypridle.conf' }
- { src: 'hyprpaper.conf.j2', dest: 'hypr/hyprpaper.conf' }
@@ -25,6 +25,7 @@ input {
kb_layout = de
follow_mouse = 1
sensitivity = 0
numlock_by_default = true
}
device {
@@ -66,6 +67,7 @@ $mainMod = SUPER
bind = $mainMod, Q, killactive,
bind = $mainMod, T, exec, kitty
bind = $mainMod, RETURN, exec, kitty
bind = $mainMod, M, exit,
bind = $mainMod, E, exec, kitty -e yazi
bind = $mainMod, V, togglefloating,
@@ -0,0 +1,194 @@
-- =========================================================================
-- hyprland.lua - managed by Ansible
-- Hyprland 0.55+ Lua config for mw-pfeddersheim-workstation
-- SUPER = Windows key
-- =========================================================================
local terminal = "kitty"
local fileManager = "kitty -e yazi"
local menu = "hyprlauncher"
local browser = "google-chrome-stable"
local mainMod = "SUPER"
-----------------
---- MONITORS ----
-----------------
-- 1. DP-1 TERRA 3280W 2560x1440@144 (0x0)
-- 2. DVI-D-1 BenQ BL2405 1920x1080@60 (rotated 90 CW)
-- 3. HDMI-A-1 TERRA 3280W 2560x1440@144 (3640x0)
hl.monitor({ output = "DP-1", mode = "2560x1440@60", position = "0x0", scale = "1" })
hl.monitor({ output = "DVI-D-1", mode = "1920x1080@60", position = "2560x0", scale = "1", transform = 1 })
hl.monitor({ output = "HDMI-A-1", mode = "2560x1440@60", position = "3640x0", scale = "1" })
hl.workspace_rule({ workspace = 1, monitor = "DP-1", default = true })
hl.workspace_rule({ workspace = 2, monitor = "DVI-D-1", default = true })
hl.workspace_rule({ workspace = 3, monitor = "HDMI-A-1", default = true })
-------------------------------
---- ENVIRONMENT VARIABLES ----
-------------------------------
hl.env("LIBVA_DRIVER_NAME", "nvidia")
hl.env("XDG_SESSION_TYPE", "wayland")
hl.env("GBM_BACKEND", "nvidia-drm")
hl.env("__GLX_VENDOR_LIBRARY_NAME", "nvidia")
hl.env("WLR_NO_HARDWARE_CURSORS", "1")
hl.env("NVD_BACKEND", "direct")
---------------
---- INPUT ----
---------------
hl.config({
input = {
kb_layout = "de",
follow_mouse = 1,
sensitivity = 0,
numlock_by_default = true,
},
})
hl.device({ name = "pixart-usb-optical-mouse", left_handed = 1 })
-----------------------
---- LOOK AND FEEL ----
-----------------------
hl.config({
general = {
gaps_in = 5, gaps_out = 10, border_size = 2,
col = {
active_border = { colors = {"rgba(33ccffee)", "rgba(00ff99ee)"}, angle = 45 },
inactive_border = "rgba(595959aa)",
},
resize_on_border = true, allow_tearing = false, layout = "dwindle",
},
decoration = {
rounding = 10, rounding_power = 2,
active_opacity = 1.0, inactive_opacity = 1.0,
shadow = { enabled = true, range = 4, render_power = 3, color = 0xee1a1a1a },
blur = { enabled = true, size = 3, passes = 1, vibrancy = 0.1696 },
},
animations = { enabled = true },
})
hl.curve("easeOutQuint", { type = "bezier", points = { {0.23,1},{0.32,1} } })
hl.curve("easeInOutCubic", { type = "bezier", points = { {0.65,0.05},{0.36,1} } })
hl.curve("linear", { type = "bezier", points = { {0,0},{1,1} } })
hl.curve("almostLinear", { type = "bezier", points = { {0.5,0.5},{0.75,1} } })
hl.curve("quick", { type = "bezier", points = { {0.15,0},{0.1,1} } })
hl.curve("easy", { type = "spring", mass = 1, stiffness = 71.2633, dampening = 15.8273644 })
hl.animation({ leaf = "global", enabled = true, speed = 10, bezier = "default" })
hl.animation({ leaf = "border", enabled = true, speed = 5.39, bezier = "easeOutQuint" })
hl.animation({ leaf = "windows", enabled = true, speed = 4.79, spring = "easy" })
hl.animation({ leaf = "windowsIn", enabled = true, speed = 4.1, spring = "easy", style = "popin 87%" })
hl.animation({ leaf = "windowsOut", enabled = true, speed = 1.49, bezier = "linear", style = "popin 87%" })
hl.animation({ leaf = "fadeIn", enabled = true, speed = 1.73, bezier = "almostLinear" })
hl.animation({ leaf = "fadeOut", enabled = true, speed = 1.46, bezier = "almostLinear" })
hl.animation({ leaf = "fade", enabled = true, speed = 3.03, bezier = "quick" })
hl.animation({ leaf = "layers", enabled = true, speed = 3.81, bezier = "easeOutQuint" })
hl.animation({ leaf = "layersIn", enabled = true, speed = 4, bezier = "easeOutQuint", style = "fade" })
hl.animation({ leaf = "layersOut", enabled = true, speed = 1.5, bezier = "linear", style = "fade" })
hl.animation({ leaf = "fadeLayersIn", enabled = true, speed = 1.79, bezier = "almostLinear" })
hl.animation({ leaf = "fadeLayersOut", enabled = true, speed = 1.39, bezier = "almostLinear" })
hl.animation({ leaf = "workspaces", enabled = true, speed = 1.94, bezier = "almostLinear", style = "fade" })
hl.animation({ leaf = "workspacesIn", enabled = true, speed = 1.21, bezier = "almostLinear", style = "fade" })
hl.animation({ leaf = "workspacesOut", enabled = true, speed = 1.94, bezier = "almostLinear", style = "fade" })
hl.animation({ leaf = "zoomFactor", enabled = true, speed = 7, bezier = "quick" })
hl.config({ dwindle = { preserve_split = true } })
hl.config({ master = { new_status = "master" } })
hl.config({ misc = { force_default_wallpaper = 0, disable_hyprland_logo = true } })
---------------------
---- KEYBINDINGS ----
---------------------
-- Core binds
hl.bind(mainMod .. " + Q", hl.dsp.window.close())
hl.bind(mainMod .. " + T", hl.dsp.exec_cmd(terminal))
hl.bind(mainMod .. " + RETURN", hl.dsp.exec_cmd(terminal))
hl.bind(mainMod .. " + M", hl.dsp.exec_cmd("command -v hyprshutdown >/dev/null 2>&1 && hyprshutdown || hyprctl dispatch 'hl.dsp.exit()'"))
hl.bind(mainMod .. " + E", hl.dsp.exec_cmd(fileManager))
hl.bind(mainMod .. " + V", hl.dsp.window.float({ action = "toggle" }))
hl.bind(mainMod .. " + D", hl.dsp.exec_cmd(menu))
hl.bind(mainMod .. " + P", hl.dsp.window.pseudo())
hl.bind(mainMod .. " + J", hl.dsp.layout("togglesplit"))
-- Worktime safers
hl.bind(mainMod .. " + F", hl.dsp.window.fullscreen({ mode = 0 }))
hl.bind(mainMod .. " + R", hl.dsp.exec_cmd("hyprctl reload"))
hl.bind(mainMod .. " + SPACE", hl.dsp.window.float({ action = "toggle" }))
hl.bind(mainMod .. " + TAB", hl.dsp.exec_cmd("hyprctl dispatch cyclenext"))
hl.bind(mainMod .. " + SHIFT + TAB", hl.dsp.exec_cmd("hyprctl dispatch cyclenext prev"))
-- Scratchpad
hl.bind(mainMod .. " + GRAVE", hl.dsp.workspace.toggle_special("magic"))
hl.bind(mainMod .. " + SHIFT + GRAVE", hl.dsp.window.move({ workspace = "special:magic" }))
-- Home row focus
hl.bind(mainMod .. " + H", hl.dsp.focus({ direction = "left" }))
hl.bind(mainMod .. " + L", hl.dsp.focus({ direction = "right" }))
-- Home row move window
hl.bind(mainMod .. " + SHIFT + H", hl.dsp.window.move({ direction = "left" }))
hl.bind(mainMod .. " + SHIFT + L", hl.dsp.window.move({ direction = "right" }))
hl.bind(mainMod .. " + SHIFT + K", hl.dsp.window.move({ direction = "up" }))
hl.bind(mainMod .. " + SHIFT + J", hl.dsp.window.move({ direction = "down" }))
-- Home row resize
hl.bind(mainMod .. " + ALT + H", hl.dsp.exec_cmd("hyprctl dispatch resizeactive -40 0"))
hl.bind(mainMod .. " + ALT + L", hl.dsp.exec_cmd("hyprctl dispatch resizeactive 40 0"))
hl.bind(mainMod .. " + ALT + K", hl.dsp.exec_cmd("hyprctl dispatch resizeactive 0 -40"))
hl.bind(mainMod .. " + ALT + J", hl.dsp.exec_cmd("hyprctl dispatch resizeactive 0 40"))
-- Arrow focus
hl.bind(mainMod .. " + left", hl.dsp.focus({ direction = "left" }))
hl.bind(mainMod .. " + right", hl.dsp.focus({ direction = "right" }))
hl.bind(mainMod .. " + up", hl.dsp.focus({ direction = "up" }))
hl.bind(mainMod .. " + down", hl.dsp.focus({ direction = "down" }))
-- Arrow move window
hl.bind(mainMod .. " + SHIFT + left", hl.dsp.window.move({ direction = "left" }))
hl.bind(mainMod .. " + SHIFT + right", hl.dsp.window.move({ direction = "right" }))
hl.bind(mainMod .. " + SHIFT + up", hl.dsp.window.move({ direction = "up" }))
hl.bind(mainMod .. " + SHIFT + down", hl.dsp.window.move({ direction = "down" }))
-- Lock, browser
hl.bind(mainMod .. " + CTRL + L", hl.dsp.exec_cmd("hyprlock"))
hl.bind(mainMod .. " + B", hl.dsp.exec_cmd(browser))
-- Workspaces 1-9
for i = 1, 9 do
hl.bind(mainMod .. " + " .. i, hl.dsp.focus({ workspace = i }))
hl.bind(mainMod .. " + SHIFT + " .. i, hl.dsp.window.move({ workspace = i }))
end
-- Screenshots
hl.bind("Print", hl.dsp.exec_cmd("grimblast copysave area"))
hl.bind(mainMod .. " + Print", hl.dsp.exec_cmd("grimblast copysave active"))
hl.bind(mainMod .. " + SHIFT + Print", hl.dsp.exec_cmd("grimblast copysave output"))
hl.bind(mainMod .. " + ALT + Print", hl.dsp.exec_cmd('grim -g "$(slurp)" - | satty -f -'))
-- Mouse binds
hl.bind(mainMod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true })
hl.bind(mainMod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true })
--------------------------------
---- WINDOW RULES ----
--------------------------------
hl.window_rule({
name = "suppress-maximize-events",
match = { class = ".*" },
suppress_event = "maximize",
})
hl.window_rule({
name = "fix-xwayland-drags",
match = { class = "^$", title = "^$", xwayland = true, float = true, fullscreen = false, pin = false },
no_focus = true,
})
+1
View File
@@ -1,5 +1,6 @@
# mw-pfeddersheim-workstation variables
system_user: mw
local_user: "{{ system_user }}"
root_filesystem_uuid: 2aeade2e-b169-4964-8cbd-bb7356320b51
root_filesystem_mount_opts: noatime
swap_file_path: /swapfile
+1 -1
View File
@@ -14,7 +14,7 @@ alias k='kubectl'
alias main='bash {{ workspace_root }}/scripts/maintenance.sh'
# Environment
export EDITOR='vim'
export EDITOR='nvim'
export PATH="$HOME/.local/bin:$PATH"
# saTway indicators
+36 -24
View File
@@ -1,7 +1,7 @@
---
description: >-
Decision to dual-install linux612 (LTS) + linux70 (stable) kernels and
migrate NVIDIA from the pinned 575xx branch to the mainline 595.71 branch
Decision to dual-install linux618 (LTS) + linux70 (stable) kernels and
migrate NVIDIA from the pinned 575xx branch to the 580xx branch (NFB)
to prepare the workstation for Hyprland/Wayland.
tags:
- adr
@@ -9,13 +9,13 @@ tags:
- nvidia
- hyprland
- wayland
last_updated: '2026-05-18'
last_updated: '2026-06-17'
---
# ADR 2: Kernel and NVIDIA Driver Strategy for Hyprland/Wayland
## Status
Accepted (2026-05-18)
Accepted (2026-05-18) — Updated (2026-06-17)
## Context
The workstation runs Manjaro Linux on AMD Ryzen 7 2700X with an NVIDIA
@@ -25,14 +25,14 @@ Wayland. Two preconditions must hold before that switch is safe:
1. **DRM/atomic and NVIDIA explicit-sync support** — Hyprland on NVIDIA
relies on driver-level explicit sync (introduced in 555.x and matured
through 575.x and 595.x). Without it, flickering and missed vblanks
through 575.x, 580.x, and 595.x). Without it, flickering and missed vblanks
are very common on Pascal+multi-monitor.
2. **Matching kernel modules** — Manjaro ships pre-built `*-nvidia`
modules pinned to a single `nvidia-utils` version. Mixing `575xx`
userspace with a kernel that only has `595.71` modules (or vice
userspace with a kernel that only has `580xx` modules (or vice
versa) breaks the driver at the next kernel boot.
The starting point at the time of this ADR:
The starting point at the time of this ADR (2026-05-18):
| Item | Version |
|---------------------------|------------------------|
@@ -43,17 +43,29 @@ The starting point at the time of this ADR:
| `linux70-nvidia` available | `595.71.05-0.1` |
| `linux612-nvidia` (mainline) | `595.71.05-2` |
Current state (2026-06-17):
| Item | Version |
|---------------------------|------------------------|
| Kernel packages | `linux618`, `linux70` |
| NVIDIA driver branch | `580xx` (NVIDIA Fast Branch) |
| NVIDIA kernel module pkg | `linux618-nvidia-580xx`, `linux70-nvidia-580xx` |
| NVIDIA userspace packages | `nvidia-580xx-utils`, `lib32-nvidia-580xx-utils`, `opencl-nvidia-580xx`, `nvidia-580xx-settings` |
| Legacy packages removed | `*-nvidia-575xx` family (see `nvidia_packages_to_remove`) |
## Decision
1. **Run two kernels side by side.**
- Keep `linux612` (LTS 6.12.x) installed as the *fallback* boot entry.
- Keep `linux618` (LTS 6.18.x) installed as the *fallback* boot entry.
- Install `linux70` (stable 7.0.x) and select it as the daily-driver
kernel via GRUB at the next reboot.
2. **Migrate NVIDIA from the `575xx` branch to mainline `595.71`** in
2. **Migrate NVIDIA from the `575xx` branch to `580xx` (NVIDIA Fast Branch)** in
one atomic pacman transaction so both kernels have matching modules:
- Remove: `linux612-nvidia-575xx`, `nvidia-575xx-utils`,
`lib32-nvidia-575xx-utils`, `nvidia-575xx-settings`.
- Install: `linux612-nvidia`, `linux70-nvidia`, `nvidia-utils`,
`lib32-nvidia-utils`, `opencl-nvidia`, `nvidia-settings`.
`lib32-nvidia-575xx-utils`, `nvidia-575xx-settings`,
and the entire `*-nvidia-575xx` family.
- Install: `linux618-nvidia-580xx`, `linux70-nvidia-580xx`,
`nvidia-580xx-utils`, `lib32-nvidia-580xx-utils`,
`opencl-nvidia-580xx`, `nvidia-580xx-settings`.
3. **Do not change `GRUB_DEFAULT` programmatically.** The user picks
`linux70` from the GRUB menu on first boot. If `linux70` misbehaves,
the next reboot is one menu selection away from the proven LTS path.
@@ -63,17 +75,18 @@ The starting point at the time of this ADR:
`--tags system-upgrade` and is safe on every subsequent run.
## Rationale
- **LTS as a parachute.** 6.12 is a Linux LTS series with a multi-year
- **LTS as a parachute.** 6.18 is a Linux LTS series with a multi-year
support window. Keeping it installed makes the migration reversible by
one keystroke in GRUB.
- **7.0 as the daily driver.** The 7.0 series brings the maturest
DRM/atomic-modeset and NVIDIA explicit-sync paths that Hyprland on
Pascal needs to avoid flicker on multi-monitor.
- **Mainline 595.71 over pinned 575.64.05.** Manjaro builds
`linux70-nvidia` only against `nvidia-utils=595.71.05`, so the 575xx
- **580xx over 595.71.** The NVIDIA Fast Branch (580xx) provides newer
driver improvements while maintaining backward compatibility. Manjaro
builds `linux70-nvidia` against `nvidia-580xx-utils`, so the 575xx
branch is not an option for the new kernel. Pascal (GP107) is fully
supported by 595.x, and 595.x ships the latest explicit-sync /
modesetting fixes upstream.
supported by 580.x, which ships explicit-sync / modesetting fixes
upstream.
- **Single atomic transaction.** Removing the old branch and installing
the new one in one pacman call (with the removal list narrowed to
packages that are *actually* installed) keeps the GPU-driver window
@@ -85,24 +98,23 @@ The starting point at the time of this ADR:
## Consequences
- **Two kernels on `/` partition.** Roughly +120 MB for `linux70` plus
~50 MB for `linux70-nvidia`. The role pre-flight refuses to start if
~50 MB for `linux70-nvidia-580xx`. The role pre-flight refuses to start if
free space on `/` is below 5 GiB.
- **`mhwd` profile referencing `575xx` stays as-is.** Only the
kernel-bound `linux612-nvidia-575xx` and matching userspace are
swapped; `mhwd-nvidia-575xx` stays installed so MHWD's profile
listing remains consistent. A future cleanup can switch the MHWD
profile to `video-nvidia` once `linux70` is verified.
kernel-bound `linux618-nvidia-580xx` and matching userspace are
installed; legacy `*-nvidia-575xx` packages are removed.
- **Manual reboot required.** The role never reboots the machine; it
ends with a clear notice instructing the user to boot into `linux70`
from GRUB before applying the next (Hyprland) playbook.
- **Future kernel pin bumps are one-line changes** in
- **Future kernel/driver pin bumps are one-line changes** in
`ansible/vars/main.yml` (`kernel_packages` /
`nvidia_kernel_modules` lists).
`nvidia_kernel_modules` / `nvidia_userspace_packages` lists).
## References
- Hyprland NVIDIA notes (wiki.hypr.land/Nvidia): explicit sync
available from driver ≥555 and required for tear-free Wayland.
- `docs/tech/hardware-inventory.md` (NVIDIA section).
- `ansible/roles/system-upgrade/tasks/main.yml`.
- `ansible/vars/main.yml` (current kernel and NVIDIA package pins).
- ADR 1: `docs/adr/0001-use-ansible-for-configuration.md` — anchors the
"Ansible-first" guardrail this decision follows.
-120
View File
@@ -1,120 +0,0 @@
# Hyprland Keybindings — Full Cheatsheet
**Layout:** German (de) | **Mod key:** SUPER (Windows key) | **Source:** `hyprctl binds` live
---
## General
| Shortcut | Action | Details |
|----------|--------|---------|
| `SUPER+Q` | Kill active window | killactive |
| `SUPER+T` | Open terminal | kitty |
| `SUPER+M` | Exit Hyprland | exit |
| `SUPER+E` | File manager | dolphin |
| `SUPER+V` | Toggle floating | togglefloating |
| `SUPER+D` | App launcher | hyprlauncher |
| `SUPER+P` | Toggle pseudo-split | pseudo |
| `SUPER+J` | Toggle split direction | layoutmsg togglesplit |
| `SUPER+F` | Toggle fullscreen | fullscreen 0 |
| `SUPER+R` | Reload config | hyprctl reload |
| `SUPER+SPACE` | Toggle float (popup) | togglefloating |
| `SUPER+TAB` | Cycle next window | cyclenext |
| `SUPER+SHIFT+TAB` | Cycle previous window | cyclenext prev |
| `SUPER+` `` ` `` (Grave) | Toggle scratchpad | togglespecialworkspace |
| `SUPER+SHIFT+` `` ` `` | Send window to scratchpad | movetoworkspace special |
| `SUPER+CTRL+K` | Keybind cheat menu | *(broken — pipelines to grep+fuzzel)* |
| `SUPER+CTRL+L` | Lock screen | hyprlock |
| `SUPER+B` | Web browser | google-chrome-stable |
## Navigation / Focus
| Shortcut | Action |
|----------|--------|
| `SUPER+TAB` | Cycle next window |
| `SUPER+SHIFT+TAB` | Cycle previous window |
| `SUPER+H` | Move focus left |
| `SUPER+L` | Move focus right |
| `SUPER+left arrow` | Move focus left |
| `SUPER+right arrow` | Move focus right |
| `SUPER+up arrow` | Move focus up |
| `SUPER+down arrow` | Move focus down |
## Window Movement
| Shortcut | Action |
|----------|--------|
| `SUPER+SHIFT+H` | Move window left |
| `SUPER+SHIFT+L` | Move window right |
| `SUPER+SHIFT+K` | Move window up |
| `SUPER+SHIFT+J` | Move window down |
| `SUPER+SHIFT+left` | Move window left |
| `SUPER+SHIFT+right` | Move window right |
| `SUPER+SHIFT+up` | Move window up |
| `SUPER+SHIFT+down` | Move window down |
## Window Resize
| Shortcut | Action |
|----------|--------|
| `SUPER+ALT+H` | Resize active: width -40 |
| `SUPER+ALT+L` | Resize active: width +40 |
| `SUPER+ALT+K` | Resize active: height -40 |
| `SUPER+ALT+J` | Resize active: height +40 |
## Screenshots
| Shortcut | Action | Tool |
|----------|--------|------|
| `Print` | Area select → save + copy | grimblast copysave area |
| `SUPER+Print` | Active window → save + copy | grimblast copysave active |
| `SUPER+SHIFT+Print` | Full monitor → save + copy | grimblast copysave output |
| `SUPER+ALT+Print` | Area select → annotate | grim | slurp | satty |
## Workspaces
| Shortcut | Action |
|----------|--------|
| `SUPER+1..9` | Switch to workspace 1-9 |
| `SUPER+SHIFT+1..9` | Move active window to workspace 1-9 |
| `SUPER+` `` ` `` | Toggle scratchpad (togglespecialworkspace) |
| `SUPER+SHIFT+` `` ` `` | Move to scratchpad (movetoworkspace special) |
### Workspace Layout
| Workspace | Monitor | Position |
|-----------|---------|----------|
| 1 | DP-1 (TERRA 3280W) | Left, 2560x1440@60 |
| 2 | DVI-D-1 (BenQ BL2405) | Center, 1920x1080@60 rotated 90deg |
| 3 | HDMI-A-1 (TERRA 3280W) | Right, 2560x1440@60 |
## Notes
- `SUPER+CTRL+K` (keybind menu) does **not work** — the `sed` pipeline breaks. Needs fix.
- The `sed` pipeline `sed 's/^bind\s*=\s*\$mainMod/SUPER/;s/\s*#/#/'` fails silently because the unescaped `$` in the `sed` command gets interpreted as a variable.
## Troubleshooting
| Issue | Check |
|-------|-------|
| Print key not working | `hyprctl binds | grep Print` |
| `SUPER+R` not working | `hyprctl binds | grep 'reload'` |
| Screenshots fail | `which grimblast satty slurp grim` |
| Clipboard empty | `echo test | wl-copy && wl-paste` |
## Satty Editor Shortcuts
| Action | Shortcut |
|--------|----------|
| Freehand | F |
| Rectangle | R |
| Ellipse | E |
| Line/Arrow | L |
| Text | T |
| Blur | B |
| Crop | C |
| Undo | Ctrl+Z |
| Redo | Ctrl+Shift+Z |
| Copy | Ctrl+C |
| Save | Ctrl+S |
| Quit | Esc or Ctrl+Q |
@@ -0,0 +1,135 @@
---
description: >-
Session record (2026-06-24) of detecting the YubiKey, planning GPG commit
signing, building a reusable SPECCOL inventory/provisioning stick, and
hardening + provisioning a Dell Latitude E7250 as a dedicated air-gapped
keygen/forensic host.
tags:
- learning
- yubikey
- gpg
- security
- air-gap
- latitude-e7250
last_updated: '2026-06-24'
---
# YubiKey + Dell E7250 air-gapped keygen host - 2026-06-24
## Summary
Detected and inventoried the attached YubiKey; researched 2026 best practice
for hardware-backed Git commit signing; wrote a baby-step plan; then procured,
hardened, and provisioned a dedicated Dell Latitude E7250 as the permanent
air-gapped keygen/forensic host. Built a reusable `SPECCOL` USB toolkit
(inventory + provisioning scripts, verified BIOS, checklist) along the way.
**Outcome:** E7250 is fully hardened + provisioned and ready to go offline for
PGP key generation. The YubiKey remains the daily-carry signer; the Type-C USB
will hold the LUKS-encrypted key recovery backup (pending keygen).
## Decisions (with rationale)
| Decision | Choice | Why |
|----------|--------|-----|
| Signing approach | **GPG/OpenPGP on YubiKey** (not SSH `ed25519-sk`) | Secret keys are exportable -> a true LUKS recovery backup on USB is possible (requirement #3). |
| Keygen environment | **Dedicated air-gapped host** (Dell E7250) instead of a re-booted live USB | Reproducible; Certify key never touches a networked machine; reusable for forensic work. |
| Key identity | `Michael Wegener <mw@satware.com>`; subkeys ed25519(S)/cv25519(E)/ed25519(A); 2y expiry; touch required | 2026 modern; avoids RSA-deprecation for SSH auth. |
| Push transport | HTTPS + GPG-sign (unchanged) | Lowest risk; signing is transport-independent. |
## Components & final state
### 1. YubiKey (serial 30836069, FW 5.7.1, USB-A)
- OTP: slot 1 programmed, slot 2 empty.
- FIDO2: PIN set (8 tries left), 4 credentials - `google.com` (Jane Alesi),
`auth.satware.ai`, `git.g-b-c.de`, `github.com` (satwareAG-ironMike).
- OATH: empty. PIV: factory-defaults/empty. OpenPGP: **empty, sign-PIN was blocked**.
- Note: existing GPG keyring has 3 **expired** ed25519 keys tied to a *different*
card serial (24530904) - stale references, not on this key.
### 2. Plan (committed)
- `docs/plans/2026-06-24-yubikey-gpg-github-signing.md` (commit `5a714e6`).
- 9 phases (0-9): remediate -> air-gapped keygen -> LUKS backup -> transfer to
YubiKey -> configure workstation -> register with GitHub -> Ansible -> recovery
-> 2nd YubiKey.
### 3. Boot stick (Type-C USB, 29 GB)
- Debian Live **13.5.0 Trixie XFCE** ISO, verified (SHA512 + Dell... Debian CD
OpenPGP signature), `dd`'d clean at ~40 MB/s.
### 4. SPECCOL toolkit stick (FAT32, label `SPECCOL`, ~1.9 GB) - reusable
```
toolkit/collect-specs.sh dock-aware inventory (TPM version, radios,
TB dock nvm/serial, fwupd, robust tooling check)
devices/latitude-e7250/provision.sh apt update + full-upgrade + tooling + autoremove
+ logging + re-inventory (idempotent)
devices/latitude-e7250/checklist.md hardening + provisioning checklist
devices/latitude-e7250/bios/ E7250A24.exe (verified) + flash instructions
devices/latitude-e7250/logs/ one provision log per run
inventory/<hostname>/ collected hw-specs baselines
README.txt incl. FAT32 "+x" rule (run via `bash`)
```
### 5. Dell Latitude E7250 (Service Tag JQN3P32) - the air-gapped host
- i7-5600U (vPro), 16 GB RAM, SK hynix SC210 mSATA 256 GB SSD (TRIM), Intel HD 5500.
- Debian 13 Trixie, kernel **6.12.94** (dist-upgraded), LUKS on root+swap.
- **Hardened:** BIOS **A24**; Secure Boot **enabled** (+ kernel lockdown); AMT/vPro
**disabled** (MEBx); Wi-Fi **off** (gone from bus); Bluetooth **hard-blocked**;
WWAN (Dell Gobi 5809e 4G LTE) **off**; TPM **cleared -> 1.2-only** (hardware
limit - no 2.0 option in BIOS A24; non-blocking).
- **Provisioned:** full dist-upgrade applied; 20/20 Phase-2 tools installed
(scdaemon, pcscd, pcsc-tools, yubikey-manager/ykman, yubikey-personalization,
cryptsetup, parted, fdisk, dosfstools, e2fsprogs, paperkey, tpm2-tools, fwupd,
hdparm, smartmontools, nvme-cli, usbutils, pciutils, rfkill). 0 packages pending.
- **Dock:** Dell E-Port Replicator (legacy bottom-connector; SMSC USB hubs only;
no Thunderbolt, no serial, no updatable firmware).
## Architecture (refined)
- **E7250** (offline, LUKS) = keygen + recovery host; holds the Certify key.
- **YubiKey** = daily-carry commit/tag signer on the workstation.
- **Type-C USB** = offline LUKS-encrypted recovery backup of the keys (pending Phase 3).
## Key learnings / gotchas
- **LVFS:** the E7250 is **NOT** on the LVFS - no `fwupd`/`.cab` BIOS path; Dell
dock also not updatable. BIOS update only via the Dell `.exe` (F12 BIOS Flash
Update or FreeDOS).
- **Dell package naming:** the YubiKey Manager CLI is Debian package
**`yubikey-manager`** (provides the `ykman` binary), **not** `ykman`. One invalid
name aborts the entire `apt-get install`. Fixed in `provision.sh`.
- **FAT32 +x:** the SPECCOL stick can't store the executable bit - scripts must be
invoked as `bash <script>` (the toolkit scripts chain each other via `bash`).
- **TXT vs TPM:** Intel TXT must be **disabled before clearing the TPM** ("TXT must
be disabled before clearing"). TXT is not needed for this host - left disabled.
- **TPM version:** `/sys/class/tpm/tpm0/tpm_version_major` (kernel) is the reliable
TPM 1.2-vs-2.0 indicator; `/dev/tpmrm0` presence also signals TPM 2.0.
- **OpenPGP sign-PIN block** was pre-existing; a `ykman openpgp reset` will clear it
before key transfer (Phase 4).
- **gh auth** token for satwareAG-ironMike is invalid on the workstation; git
protocol is HTTPS. (Re-auth when starting workstation-side setup.)
- **Plaintext secrets exposure:** a `yubikey-provision-…` folder with unencrypted
GnuPG + SSH private keys was on the Type-C USB ("safety net"). It gets destroyed
when that stick is repartitioned for the LUKS backup (Phase 3) - same keys exist
on the workstation `~/.gnupg`/`~/.ssh`, so nothing is lost.
## Verification baselines captured (on the SPECCOL stick)
`inventory/michael-latitudee7250/hw-specs-*.txt` across: pre-flash (A19) ->
post-flash (A24) -> post-provision -> post-TPM-clear. Tooling-detection bugs
(scdaemon/pcsc-tools/yubikey-personalization binary-name mismatches; ambiguous
lsusb messages; ANSI codes from fwupd) were fixed in `collect-specs.sh`.
## Open items / next steps
1. On the E7250: (optional) set a BIOS/Setup admin password; **unplug Ethernet
permanently**.
2. **Phase 2 - keygen** on the E7250 (tmpfs GNUPGHOME): Certify key + S/E/A
subkeys. Then Phase 3 (LUKS backup to Type-C USB `sdc3`) and Phase 4 (transfer
to YubiKey; reset OpenPGP app first to clear the blocked sign-PIN).
3. Workstation side: `gh auth login`, import public key, configure `git` signing
(Phases 5-6), fold non-secret config into `ansible/` (Phase 7).
4. Procure a 2nd YubiKey for redundancy (Phase 9).
## Artifacts produced today
- `docs/plans/2026-06-24-yubikey-gpg-github-signing.md` (commit `5a714e6`)
- `docs/learnings/2026-06-24-yubikey-e7250-airgap-setup.md` (this file)
- SPECCOL toolkit stick (toolkit + devices/latitude-e7250/* + verified BIOS A24)
- Type-C USB = current Debian-Live boot stick (29 GB; to be repurposed as the
LUKS recovery backup in Phase 3)
@@ -0,0 +1,163 @@
---
description: >-
Session record (2026-07-03): diagnosed a Remmina SIGSEGV on Wayland when
keymap was set, traced it to a missing NULL check on XOpenDisplay in
remmina-plugin-rdp, forked Remmina to gitlab.com/satware, built from
source, verified the fix, and submitted MR !2757 upstream.
tags:
- learning
- remmina
- wayland
- rdp
- crash-analysis
- gitlab
- upstream-contribution
last_updated: '2026-07-03'
---
# Remmina SIGSEGV on Wayland - diagnosis, fix, upstream MR - 2026-07-03
## Summary
Remmina crashed (SIGSEGV) every time the "Mothership" RDP profile was
selected. The root cause was a missing NULL check on `XOpenDisplay(0)` in
`remmina_get_rdp_kbd_remap()` (`plugins/rdp/rdp_plugin.c`), triggered only
when a `keymap` profile setting was non-empty (e.g. `Map Meta Keys`) and
the session was Wayland without accessible X11.
Forked Remmina to `gitlab.com/satware/Remmina`, built from source with
FreeRDP3, reproduced the crash, applied a 4-line fix, verified it, and
submitted merge request [!2757](https://gitlab.com/Remmina/Remmina/-/merge_requests/2757)
to the upstream project.
## Incident
| Field | Value |
|-------|-------|
| Symptom | Remmina closes immediately when selecting "Mothership" and clicking connect |
| Signal | SIGSEGV (segfault) |
| Crash location | `XKeysymToKeycode()` in `libX11.so.6`, called from `remmina-plugin-rdp.so` |
| Profile | `group_rdp_mothership_10-10-202-4.remmina` (server `10.10.202.4`) |
| Working profile | `srv-app-2` (`100.64.0.78`) - identical protocol, no crash |
## Diagnosis
### Stack trace (from coredumpctl)
```
#0 XKeysymToKeycode (libX11.so.6 + 0x92cd7)
#1 remmina-plugin-rdp.so + 0xff27
#2 remmina-plugin-rdp.so + 0x1296b
```
### Root cause
In `plugins/rdp/rdp_plugin.c`, function `remmina_get_rdp_kbd_remap()`:
```c
display = XOpenDisplay(0); // Returns NULL on Wayland
// NO NULL CHECK
XKeysymToKeycode(display, table[i]) // SIGSEGV - dereferences NULL
```
The function is only called when `keymap` is non-empty in the profile.
The working profile had `keymap=` (empty), which skipped the entire code
path. The crashing profile had `keymap=Map Meta Keys`.
### Environment
| Component | Version |
|-----------|---------|
| OS | Manjaro Linux, Wayland session (`XDG_SESSION_TYPE=wayland`) |
| Remmina | 1.4.43 (git n/a) |
| FreeRDP | 3.27.0 |
| Display | `DISPLAY=:0` (Xwayland) but `XOpenDisplay(0)` fails from RDP worker thread |
### Key config difference
| Setting | Mothership (crashes) | srv-app-2 (works) |
|---------|---------------------|-------------------|
| `keymap` | `Map Meta Keys` | (empty) |
## Fix
4-line addition in `plugins/rdp/rdp_plugin.c`, function `remmina_get_rdp_kbd_remap()`:
```diff
display = XOpenDisplay(0);
+if (!display) {
+ g_free(rdp_kbd_remap);
+ return NULL;
+}
for (i = 0; table[i] > 0; i += 2) {
```
The caller at line ~2148 already checks `if (rdp_kbd_remap != NULL)` before
use, so returning NULL causes graceful degradation (empty
`KeyboardRemappingList`) instead of a crash.
## Verification
| Test | Result |
|------|--------|
| Reproduce crash (unpatched, `keymap=Map Meta Keys`) | SIGSEGV - new coredump at 12:05:54 |
| Apply fix, rebuild, same profile | No crash - exit code 0, no new coredump |
| Debug log after fix | `rdp_keyboard_remapping_list:` (empty, as expected) |
## Local workaround (applied)
| Item | Value |
|------|-------|
| Mothership profile `keymap` | Set to empty (`keymap=`) as safe setting |
| Patched plugin | `~/.config/remmina/plugins/remmina-plugin-rdp.so` (loads before system plugin) |
| Source build | `~/external/remmina/` (branch `fix/rdp-xopendisplay-null-check`) |
The patched plugin in `~/.config/remmina/plugins/` protects the system
Remmina (`/usr/bin/remmina`) even if `keymap` is set to a non-empty value.
## Upstream contribution
### GitLab.com setup (one-time)
| Item | Value |
|------|-------|
| GitLab.com group | `gitlab.com/satware` (public, ID 136548341) |
| User | `ironmikechw` (ID 39970188, Owner of `satware` group) |
| PAT | Stored in `~/.env` as `GITLAB_COM_API_TOKEN` |
| glab CLI | Authenticated to both `gitlab.satware.com` and `gitlab.com` |
### Fork and MR
| Item | Value |
|------|-------|
| Upstream | `gitlab.com/Remmina/Remmina` (canonical, project ID 7153509) |
| Fork | `gitlab.com/satware/Remmina` (project ID 84057632) |
| Local clone | `~/external/remmina/` with `upstream` remote added |
| Build | `cmake -Bbuild -H. -DCMAKE_BUILD_TYPE=Debug -DWITH_FREERDP3=ON` |
| Branch | `fix/rdp-xopendisplay-null-check` |
| Commit | `e6084bd7f` - "rdp: fix SIGSEGV when XOpenDisplay returns NULL on Wayland" |
| MR | [!2757](https://gitlab.com/Remmina/Remmina/-/merge_requests/2757) (opened, target: master) |
### Build notes
- CMake flag `-DWITH_FREERDP3=ON` is required (default is OFF/FreeRDP2)
- All build deps present on this workstation (FreeRDP3 3.27.0, GTK3 3.24.52, etc.)
- Build time: ~30 seconds (incremental, 16 cores)
- Plugin loads from `~/.config/remmina/plugins/` (alt dir, checked before system)
## Files touched
| File | Change |
|------|--------|
| `~/.local/share/remmina/group_rdp_mothership_10-10-202-4.remmina` | `keymap=Map Meta Keys` -> `keymap=` (safe setting) |
| `~/.config/remmina/plugins/remmina-plugin-rdp.so` | Patched plugin (built from source) |
| `~/external/remmina/plugins/rdp/rdp_plugin.c` | 4-line NULL check fix (committed + pushed) |
| `~/.env` | `GITLAB_COM_API_TOKEN` added (for glab CLI) |
| `~/.config/glab-cli/config.yml` | gitlab.com host added (glab auth login) |
## References
- MR: https://gitlab.com/Remmina/Remmina/-/merge_requests/2757
- Fork: https://gitlab.com/satware/Remmina
- Upstream: https://gitlab.com/Remmina/Remmina
- CONTRIBUTING.md: fork-and-pull model on GitLab
@@ -0,0 +1,208 @@
---
description: >-
Session record (2026-07-06): triaged Caddy journald warnings on
onlyoffice.localhost/mcp showing "context canceled" SSE aborts (~1.5ms)
from opencode 1.17.13 MCP client against the OnlyOffice MCP server on
:3847. Verified upstream liveness, opened GitLab issue #92 in
satware/mcp/onlyoffice, and documented the Caddy reverse-proxy as a
manual override.
tags:
- learning
- caddy
- onlyoffice
- mcp
- sse
- reverse-proxy
- investigation
last_updated: '2026-07-06'
---
# Caddy "context canceled" on onlyoffice.localhost/mcp - investigation - 2026-07-06
## Summary
Caddy (systemd `caddy.service`) on `mw-manjaro-pf` is emitting a high
volume of `warn`-level journald entries: `aborting with incomplete
response ... error: "reading: context canceled"` for `GET /mcp` requests
on `https://onlyoffice.localhost`. Each abort fires ~1.5ms after the
request is forwarded to the upstream `localhost:3847` (OnlyOffice MCP
server). The upstream is alive and correctly rejects non-SSE GETs with
HTTP 406, so this is a stream-lifecycle issue, not a server-down issue.
Investigation handed off to the OnlyOffice MCP repo as GitLab issue #92.
This workstation's Caddy reverse-proxy config is now recorded as a manual
override (not Ansible-managed).
## Incident
| Field | Value |
|-------|-------|
| Symptom | Repeated `context canceled` warnings in `journalctl -u caddy` |
| Caddy site | `https://onlyoffice.localhost` -> `reverse_proxy localhost:3847` |
| Upstream | `localhost:3847` (OnlyOffice MCP server) |
| Request | `GET /mcp` with `Accept: text/event-stream` |
| Client | `opencode/1.17.13` (MCP streamable-HTTP transport) |
| MCP protocol | `2025-11-25` |
| Abort duration | ~1.5ms (request forwarded, then read context canceled) |
| Frequency | Multiple per minute, across distinct `Mcp-Session-Id` values |
## Evidence
### Caddy log sample (recurring)
```json
{
"level": "warn",
"logger": "http.handlers.reverse_proxy",
"msg": "aborting with incomplete response",
"upstream": "localhost:3847",
"duration": 0.00173631,
"request": {
"method": "GET",
"host": "onlyoffice.localhost",
"uri": "/mcp",
"headers": {
"Accept": ["text/event-stream"],
"Mcp-Protocol-Version": ["2025-11-25"],
"Mcp-Session-Id": ["a5e859b4-01f1-4c24-81bb-78ee02d74357"],
"User-Agent": ["opencode/1.17.13"],
"Via": ["1.1 Caddy"]
}
},
"error": "reading: context canceled"
}
```
Observed `Mcp-Session-Id` values (each retried over several minutes):
`a5e859b4...`, `4bfbb912...`, `3c4a0454...`, `08397c3d...`,
`ed762f08...`, `b0fae341...`.
### Upstream liveness verification
```bash
$ ss -tlnp | grep :3847
LISTEN 0 4096 0.0.0.0:3847 0.0.0.0:*
$ curl -sS -o /dev/null -w "HTTP %{http_code} in %{time_total}s\n" \
--max-time 3 http://localhost:3847/mcp
HTTP 406 in 0.004665s
```
HTTP 406 is expected for a plain GET without the required SSE headers.
The MCP server is alive and rejecting non-SSE requests correctly.
## Diagnosis
### Root cause (not yet confirmed - under investigation)
Five candidate hypotheses, listed in the GitLab issue. The leading
suspect is **client-side disconnect**: opencode 1.17.13 opens the SSE
stream then immediately closes it (the ~1.5ms duration is too short for
a genuine read). Secondary suspects are server-side stream teardown,
MCP protocol-version mismatch (`2025-11-25`), Caddy HTTP/1.1 framing of
SSE, or a client session-pool bug creating and abandoning sessions.
### Why this workstation's concern
The Caddy reverse-proxy is the boundary that surfaces the symptom. Even
if the root cause is client- or server-side, the local config matters
because:
1. Caddy logs are being polluted with warn-level noise (masks real errors).
2. The reverse-proxy block has no SSE-specific tuning (`flush_interval`,
explicit HTTP/1.1 transport) that could mask or mitigate upstream
misbehavior.
3. The Caddy config at `/etc/caddy/conf.d/localhost-dev.conf` is **not**
Ansible-managed by this repo - it is a manual override that must be
recorded (see `docs/tech/manual-overrides.md`).
## Environment
| Component | Version / Value |
|-----------|----------------|
| Host | `mw-manjaro-pf` (mw-pfeddersheim-workstation) |
| OS | Manjaro Linux, Wayland session |
| Caddy | systemd `caddy.service`, active since 2026-07-02 |
| Caddy config | `/etc/caddy/Caddyfile` + `/etc/caddy/conf.d/localhost-dev.conf` |
| TLS cert | mkcert pair at `/etc/caddy/localhost{,-key}.pem` |
| Upstream | `localhost:3847` (OnlyOffice MCP server, process TBD) |
| MCP client | `opencode/1.17.13` |
| MCP protocol | `2025-11-25` (streamable HTTP transport) |
## *.localhost sites configured
All defined in `/etc/caddy/conf.d/localhost-dev.conf` (14 sites total):
| Site | Backend / Handler |
|------|-------------------|
| `mcp.localhost` | `reverse_proxy localhost:50880` |
| `artifacts.localhost` | `file_server browse` root `/var/www/artifacts` |
| `onlyoffice.localhost` | `reverse_proxy localhost:3847` (subject of this issue) |
| `project-1.localhost` ... `project-10.localhost` | `respond "project-N - placeholder"` (10 reserved) |
| `demo.satware.com.localhost` | `reverse_proxy localhost:38081` (webdevops/php-apache-dev container) |
## Handoff
### GitLab issue
| Item | Value |
|------|-------|
| Project | `satware/mcp/onlyoffice` (`gitlab.satware.com`) |
| Issue | [#92](https://gitlab.satware.com/satware/mcp/onlyoffice/-/work_items/92) |
| Title | Investigation: Caddy reports 'context canceled' on onlyoffice.localhost/mcp SSE stream (~1.5ms aborts) |
| Labels | `bug`, `investigation`, `mcp` |
| State | open |
The issue body contains the full evidence, 5 hypotheses, 7 investigation
tasks, and a proposed Caddy mitigation (`flush_interval -1` +
`transport http { versions 1.1 }`).
### Next actions (assigned to OnlyOffice MCP repo)
1. Identify the process listening on `:3847` (`sudo ss -tlnp | grep 3847`).
2. Collect server-side logs for the `Mcp-Session-Id` values cited above.
3. Reproduce with `curl -N -H "Accept: text/event-stream" -H "Mcp-Protocol-Version: 2025-11-25" https://onlyoffice.localhost/mcp`.
4. Check opencode 1.17.13 changelog for MCP SSE session bugs.
5. Verify Caddy `reverse_proxy` SSE tuning (proposed block in issue #92).
6. Confirm whether `notifications/initialized` round-trip completes.
7. Log the negotiated MCP protocol version server-side.
## Local documentation updates
| File | Change |
|------|-------|
| `docs/tech/manual-overrides.md` | Added row for `/etc/caddy/` config (not Ansible-managed) |
| `docs/tech/stack.md` | Added "Local Dev Services" section listing Caddy + `*.localhost` sites |
| `CHANGELOG.md` | Added `[Unreleased] -> Added` entry |
| GitLab issue #92 | Created with full evidence + hypotheses + tasks |
## Proposed Caddy mitigation (to test on this workstation)
```caddyfile
https://onlyoffice.localhost {
tls /etc/caddy/localhost.pem /etc/caddy/localhost-key.pem
reverse_proxy localhost:3847 {
flush_interval -1
transport http {
versions 1.1
}
}
}
```
`flush_interval -1` disables Caddy's buffering so SSE events flush to the
client immediately; `transport http { versions 1.1 }` pins the upstream
leg to HTTP/1.1 (Caddy logs already show HTTP/1.1 for the upstream, this
makes it explicit and avoids HTTP/2 SSE framing surprises).
Not applied yet - pending GitLab issue #92 resolution to avoid masking
the root cause.
## References
- GitLab issue: https://gitlab.satware.com/satware/mcp/onlyoffice/-/work_items/92
- Caddy config: `/etc/caddy/conf.d/localhost-dev.conf`
- Local skill: `caddy` (`~/.config/opencode/skills/caddy/SKILL.md`)
- MCP transport spec: https://modelcontextprotocol.io/specification/2025-11-25/basic/transports
- Manual overrides: `docs/tech/manual-overrides.md`
- Software stack: `docs/tech/stack.md`
@@ -0,0 +1,279 @@
---
description: >-
Baby-step plan to sign GitHub/Git commits and tags with a hardware-backed
OpenPGP key on YubiKey 5 NFC #30836069, generated in a dedicated air-gapped
Debian-Live environment, with LUKS-encrypted recovery backups on the Type-C
USB stick. Non-secret config folded into the ansible/ role; secrets via Bitwarden.
tags:
- plan
- security
- yubikey
- gpg
- github
last_updated: '2026-06-24'
---
# YubiKey + GPG signing for GitHub commits - 2026-06-24
Sign all Git commits/tags with a hardware-backed OpenPGP key whose secret
material lives on the YubiKey, and whose full recovery backup is stored
LUKS-encrypted on the Type-C USB stick.
## Goal
1. Hardware-backed commit/tag signing for GitHub (and GitLab, PGP email) using
the YubiKey OpenPGP smartcard.
2. A complete, offline recovery path: secret keys on a LUKS-encrypted USB stick.
3. Non-secret configuration managed through the repository's `ansible/` role.
## Decisions (confirmed 2026-06-24)
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Approach | **A - GPG/OpenPGP** (not SSH `ed25519-sk`) | Secret keys are exportable, so a true recovery backup on USB is possible. Matches the "pgp" requirement. |
| Keygen env | **Dedicated air-gapped** Debian-Live (internal disk disconnected) | drduh gold standard; the Certify key never touches a networked machine. |
| Key identity | `Michael Wegener <mw@satware.com>` | Matches `git config user.email/name`. |
| Algorithms | Certify `ed25519`; Subkeys `ed25519` (S), `cv25519` (E), `ed25519` (A) | 2026 modern; avoids RSA-deprecation for the SSH auth subkey. RSA-4096 is the fallback. |
| Touch policy | **Required** for sign + auth | Defeats malware key use; YubiKey blinks for a physical tap. |
| Subkey expiry | **2 years** (Certify never expires) | drduh lifecycle; renewable offline. |
| Push transport | **HTTPS + GPG-sign** (unchanged) | Lowest risk; signing is transport-independent. SSH-via-auth-subkey deferred. |
| Config mgmt | Fold non-secret config into `ansible/` | Repo convention; reproducible. |
| Backup target | Type-C USB `/dev/sdc` (wipe confirmed acceptable) + a 2nd USB later | Requirement #3. |
| 2nd YubiKey | Scheduled as Phase 9 follow-up (after this key works) | Redundancy + travel spare. |
## Architecture - two environments
- **Air-gapped (Debian-Live, no internal disk):** generate Certify + Subkeys,
move Subkeys onto the YubiKey, LUKS-encrypt secrets to the USB, export the
public key. The Certify key never leaves this environment.
- **This workstation (Manjaro `mw-manjaro-pf`):** import the **public key
only**, configure `git`/`gpg-agent`, sign. Private material stays on the
YubiKey.
## Pre-flight facts (current state, verified 2026-06-24)
- YubiKey 5 NFC, serial `30836069`, FW `5.7.1`, OpenPGP app **empty**,
**sign-PIN blocked (`3 0 3`)**, PIV factory-defaults/empty, OATH empty.
- FIDO2 has 4 credentials (incl. `github.com` -> `satwareAG-ironMike`).
- `gh auth` token **invalid** -> re-login required.
- USB `/dev/sdc1` = dirty FAT32 (`errors=remount-ro`) -> `fsck`, then repartition.
- Tools present: `ykman`, `ykinfo`, `gpg`, `pcscd`; `gpg-agent.conf` already has
`enable-ssh-support`.
---
## Phase 0 - Prep on this workstation (read/config only; no key material yet)
1. Fix GitHub auth + confirm tooling:
```bash
gh auth login -h github.com # re-auth (browser/token)
gh auth status
git --version # >=2.34 fine; GPG works on any
pacman -Q gnupg pcscd yubikey-manager # all present (verified earlier)
```
2. Check the USB stick holds nothing needed, then dry-run a repair:
```bash
ls "/run/media/mw/Type-C USB"
sudo fsck.vfat -n /dev/sdc1 # review output; do NOT -a yet
```
3. Identify which **spare** USB will carry the Debian-Live image (Phase 1) and
which 2nd USB will later hold the mirrored backup.
## Phase 1 - Build the air-gapped keygen USB (drduh method)
1. Download Debian-Live + signature, **verify before trusting**:
```bash
IMG=https://cdimage.debian.org/debian-cd/current-live/amd64/iso-hybrid
curl -fLO "$IMG/SHA512SUMS" -O "$IMG/SHA512SUMS.sign"
curl -fLO "$IMG/$(awk '/xfce.iso$/ {print $2}' SHA512SUMS)"
gpg --keyserver hkps://keyserver.ubuntu.com:443 \
--recv DF9B9C49EAA9298432589D76DA87E80D6294BE9B
gpg --verify SHA512SUMS.sign SHA512SUMS # MUST say "Good signature"
grep "$(sha512sum debian-live-*-amd64-xfce.iso)" SHA512SUMS
```
2. Write to a **spare** USB (not the backup stick):
```bash
sudo dd if=debian-live-*-amd64-xfce.iso of=/dev/sdX bs=4M status=progress; sync
```
## Phase 2 - Boot air-gapped, generate keys
1. Power off, **disconnect the internal NVMe**, boot the Debian-Live USB.
Network can stay off.
2. Install tools + hardened config in a **tmpfs GNUPGHOME**:
```bash
sudo apt update && sudo apt -y install gnupg2 scdaemon pcscd yubikey-manager
export GNUPGHOME=$(mktemp -d -t $(date +%Y.%m.%d)-XXXX)
cd "$GNUPGHOME"
wget https://raw.githubusercontent.com/drduh/YubiKey-Guide/master/config/gpg.conf
```
3. Create a strong **Certify passphrase** (write it down; store it apart from
the USB; record in Bitwarden):
```bash
CERTIFY_PASS=$(LC_ALL=C tr -dc "A-Z2-9" </dev/urandom | tr -d "IOUS5" \
| fold -w4 | paste -sd- - | head -c29); printf "\n$CERTIFY_PASS\n\n"
```
4. **Certify key** (never expires):
```bash
IDENTITY="Michael Wegener <mw@satware.com>"
echo "$CERTIFY_PASS" | gpg --batch --passphrase-fd 0 \
--quick-generate-key "$IDENTITY" ed25519 cert never
KEYFP=$(gpg -k --with-colons "$IDENTITY" | awk -F: '/^fpr:/{print $10; exit}')
```
5. **Subkeys** (2-year expiry): sign `ed25519`, encrypt `cv25519`, auth `ed25519`:
```bash
echo "$CERTIFY_PASS" | gpg --batch --pinentry-mode=loopback --passphrase-fd 0 --quick-add-key "$KEYFP" ed25519 sign 2y
echo "$CERTIFY_PASS" | gpg --batch --pinentry-mode=loopback --passphrase-fd 0 --quick-add-key "$KEYFP" cv25519 encrypt 2y
echo "$CERTIFY_PASS" | gpg --batch --pinentry-mode=loopback --passphrase-fd 0 --quick-add-key "$KEYFP" ed25519 auth 2y
gpg -K # expect [C] + ssb [S] [E] [A]
```
## Phase 3 - Backup to the USB stick (requirement #3) - LUKS
Use the **Type-C USB stick** (`/dev/sdc`), repartitioned: LUKS partition for
secrets + small plaintext partition for the public key.
1. Export everything:
```bash
KEYID=${KEYFP: -16}
echo "$CERTIFY_PASS" | gpg -o "$GNUPGHOME/$KEYID-Certify.key" --batch --pinentry-mode=loopback --passphrase-fd 0 --armor --export-secret-keys "$KEYID"
echo "$CERTIFY_PASS" | gpg -o "$GNUPGHOME/$KEYID-Subkeys.key" --batch --pinentry-mode=loopback --passphrase-fd 0 --armor --export-secret-subkeys "$KEYID"
gpg -o "$GNUPGHOME/$KEYID-$(date +%F).asc" --armor --export "$KEYID"
```
2. Partition + LUKS-encrypt (confirm `/dev/sdc` is the right device - destructive):
```bash
LUKS_PASS=$(LC_ALL=C tr -dc "A-Z2-9" </dev/urandom | tr -d "IOUS5" | fold -w4 | paste -sd- - | head -c29)
sudo dd if=/dev/zero of=/dev/sdc bs=4M count=1
sudo fdisk /dev/sdc <<EOF
g
n
1
+50M
n
2
w
EOF
echo "$LUKS_PASS" | sudo cryptsetup -q luksFormat /dev/sdc1
echo "$LUKS_PASS" | sudo cryptsetup luksOpen /dev/sdc1 gnupg-secrets
sudo mkfs.ext2 /dev/mapper/gnupg-secrets
sudo mkfs.vfat /dev/sdc2
sudo mkdir -p /mnt/sec /mnt/pub
sudo mount /dev/mapper/gnupg-secrets /mnt/sec
sudo mount /dev/sdc2 /mnt/pub
sudo cp -av "$GNUPGHOME" /mnt/sec/ # encrypted secrets
gpg --armor --export "$KEYID" | sudo tee "/mnt/pub/$KEYID-$(date +%F).asc" >/dev/null
sudo umount /mnt/sec /mnt/pub
sudo cryptsetup luksClose gnupg-secrets
```
3. Record `LUKS_PASS` in Bitwarden (separate from the Certify passphrase).
4. **Repeat Phase 3 on a 2nd USB** and store both offline in separate locations.
## Phase 4 - Transfer Subkeys to the YubiKey
1. Reset the OpenPGP app (sign-PIN is blocked; app is empty, so nothing is lost):
```bash
ykman openpgp reset -f # confirm when ready
gpg --card-status # retry counters should read 3 3 3
```
2. Change PINs from defaults and set cardholder identity. Generate your own
`ADMIN_PIN` (8 digits) and `USER_PIN` (6 digits); store in Bitwarden:
```bash
gpg --change-pin # menu: 3 = admin PIN (12345678 -> $ADMIN_PIN)
gpg --change-pin # menu: 1 = user PIN (123456 -> $USER_PIN)
gpg --edit-card # admin -> login -> "Michael Wegener <mw@satware.com>"
```
3. Move subkeys onto the card, then require touch:
```bash
gpg --edit-key "$KEYID" # key 1 -> keytocard (sig=1)
# key 2 -> keytocard (enc=2)
# key 3 -> keytocard (auth=3)
# save
ykman openpgp keys set-touch sig on -f -a "$ADMIN_PIN"
ykman openpgp keys set-touch dec on -f -a "$ADMIN_PIN"
ykman openpgp keys set-touch aut on -f -a "$ADMIN_PIN"
gpg -K # ssb> markers confirm subkeys are on the card
```
## Phase 5 - Configure this workstation (public key only)
1. Copy the public `.asc` from the USB plaintext partition, import, trust:
```bash
sudo mkdir -p /mnt/pub && sudo mount /dev/sdc2 /mnt/pub
gpg --import /mnt/pub/*.asc
gpg --edit-key "$KEYID" # trust -> 5 (ultimate) -> y -> save
```
2. Git signing (global; uses the **sign subkey**):
```bash
SIGNID=$(gpg -k --with-colons "$KEYID" | awk -F: '/^sub:/ && /s/ {split($0,a,":"); print a[5]}')
git config --global gpg.format openpgp
git config --global user.signingkey "$SIGNID!"
git config --global commit.gpgsign true
git config --global tag.gpgsign true
```
3. Agent/scdaemon (the `disable-ccid` line prevents the scdaemon-vs-pcscd lock
observed during detection):
```bash
echo "disable-ccid" >> ~/.gnupg/scdaemon.conf
# ~/.gnupg/gpg-agent.conf already has: enable-ssh-support, cache-ttls
gpgconf --kill gpg-agent
```
## Phase 6 - Register the public key with GitHub
```bash
gpg --armor --export "$KEYID" | gh gpg-key add - # after `gh auth login`
# or paste at github.com/settings/keys
```
Verify end-to-end: create a test commit, `git push`, the commit shows
**Verified** on GitHub.
## Phase 7 - Fold non-secret config into Ansible (repo convention)
- New role `ansible/roles/gpg_signing/` templating:
- `~/.gnupg/gpg.conf`, `~/.gnupg/gpg-agent.conf`, `~/.gnupg/scdaemon.conf`
- the four `git config --global` signing lines from Phase 5.2
- Commit the **public key** to `ansible/files/mw-satware-<KEYID>.asc` (public,
safe to track).
- **PINs, Certify and LUKS passphrases -> Bitwarden only**, never the repo
(zero-trust rule). Use Ansible Vault vars if a task must consume them.
- Add the role to `ansible/workstation.yml`; verify with
`ansible-playbook ansible/workstation.yml --check`.
## Phase 8 - Recovery & verification
- **Recovery:** plug a backup USB -> `cryptsetup luksOpen /dev/sdc1 gnupg-secrets`
-> restore `$KEYID-Subkeys.key` onto a *new* YubiKey via `keytocard`. The
Certify key is needed only to rotate/renew subkeys.
- Final verification checklist:
- `gpg --card-status` shows the three subkeys with correct fingerprints.
- `git commit -S --allow-empty -m "test"` prompts for a YubiKey tap and
succeeds.
- `git push`; GitHub shows the commit as **Verified**.
- `git tag -s v-test && git verify-tag v-test` succeeds.
## Phase 9 - Second YubiKey (follow-up, after this key works)
- Mirror the same Subkeys onto a backup YubiKey from the LUKS backup
(`$KEYID-Subkeys.key` -> `keytocard`), set identical PINs and touch policy.
- Register it on GitHub as an **additional** GPG signing key.
- Re-register its FIDO2/WebAuthn handles for the relying parties already on the
primary key (`github.com`, `auth.satware.ai`, `git.g-b-c.de`, `google.com`).
- Create a **2nd backup USB** mirroring Phase 3 and store it off-site.
## Secrets handling (per repo zero-trust rule)
| Item | Stored where |
|------|--------------|
| Public key | `ansible/files/*.asc` (committed) + USB plaintext partition |
| Certify key + Subkeys (encrypted) | LUKS partition on USB (offline, 2 copies) |
| Certify passphrase, LUKS passphrase, User PIN, Admin PIN | Bitwarden only (`bitwarden.jantec.xyz`), cached locally in `gnome-keyring` |
## References
- GitHub: About commit signature verification (GPG/SSH/S-MIME).
- GitHub: Generating a new SSH key (incl. `ed25519-sk` security-key variant).
- drduh/YubiKey-Guide (canonical GPG-on-YubiKey walkthrough; LUKS backup model).
+13 -1
View File
@@ -27,6 +27,7 @@
- `himalaya` → IMAP/SMTP passwords via `secret-tool lookup email <addr>`
- `ssh-agent` → SSH key passphrases via `secret-tool lookup ... ssh-key <path>`
- `git-credential-libsecret` → Git passwords via libsecret
(github.com via `gh`, gitlab.satware.com via `glab`, **git.satware.ai via libsecret PAT**)
- `JetBrains IDEs` → auto-detect D-Bus secrets service
## Quick Verification
@@ -42,6 +43,7 @@ rbw get "SSH Key: id_ed25519" # should return "SSH gitlab.satware.com"
# Can apps read secrets?
secret-tool lookup application ssh-agent service ssh ssh-key /home/mw/.ssh/id_ed25519
secret-tool lookup application git service gitlab.satware.com
secret-tool lookup server git.satware.ai protocol https user mw # Gitea PAT (git-credential-libsecret schema)
secret-tool lookup application nextcloud service cloud.satware.com user mw
```
@@ -51,7 +53,7 @@ secret-tool lookup application nextcloud service cloud.satware.com user mw
|----------|---------|---------|
| **SSH passphrases** | id_ed25519, id_ed25519_ja_manjaro | BW + GK |
| **SSH host passwords** | b0t@192.168.0.141 | BW + GK |
| **Git tokens** | GitLab PAT, GitHub token | BW + GK |
| **Git tokens** | GitLab PAT, GitHub token, Gitea PAT (git.satware.ai) | GK (BW pending) |
| **Remmina** | 5 VNC/RDP passwords | BW + GK |
| **Nextcloud** | 3 instance passwords | BW + GK |
| **Browser crypto** | Chrome, Brave, Chromium safe storage | BW + GK |
@@ -114,3 +116,13 @@ systemctl --user restart gnome-keyring-daemon.service # if D-Bus secrets missin
- Data file preserved at `~/.local/share/kwalletd/kdewallet.kwl` (backup)
- PAM references already commented out
- No longer used by any application
## Gitea PAT (git.satware.ai)
- Cached in gnome-keyring via `git credential approve` (schema `org.git.Password`, attributes: `protocol=https, server=git.satware.ai, user=mw`)
- Git reads it via `credential.helper=libsecret` (global; no host-specific `store` override)
- tea reads its own copy from `~/.config/tea/config.yml` (tea has no `auth git-credential` subcommand; file hardened to `chmod 600`)
- Scopes: `write:repository`, `read:user`. Token lacks admin scope - **rotation must be done via web UI** at https://git.satware.ai/user/settings/applications
- TODO (rotate): create new PAT with `write:repository, read:user, write:admin`; store in Bitwarden entry `Gitea PAT: git.satware.ai` + keyring + tea; revoke old token via API (now possible with admin scope); update this section
- TODO (Bitwarden): create Bitwarden entry `Gitea PAT: git.satware.ai` (username `mw`, uri `https://git.satware.ai`) - blocked on rbw pinentry not supporting non-interactive password entry; create manually via `rbw add` or Bitwarden web UI
- Tracked in Gitea issue #1: https://git.satware.ai/infrastructure/mw-pfeddersheim-workstation/issues/1
-102
View File
@@ -1,102 +0,0 @@
# Screenshot Cheatsheet — Hyprland + grimblast + satty
## Quick Reference (German Keyboard Layout)
| Key Combo | Action | Tool | Pipeline |
|-----------|--------|------|----------|
| `Print` | Select area → save to file + clipboard | grimblast | area → copysave |
| `SUPER + Print` | Active window → save to file + clipboard | grimblast | active window → copysave |
| `SUPER + SHIFT + Print` | Full monitor → save to file + clipboard | grimblast | output → copysave |
| `SUPER + ALT + Print` | Select area → open in satty for annotation | grim+satty | area → satty editor |
## Test Checklist
Run through these to verify everything works:
### Test 1: Area Screenshot (Print)
1. Press `Print`
2. Drag to select a region
3. Expected: Screenshot saved to `~/Pictures/` AND copied to clipboard
4. Verify: Paste clipboard into a test doc
### Test 2: Current Window (SUPER+Print)
1. Focus any window
2. Press `SUPER+Print`
3. Expected: That window captured, saved + copied
### Test 3: Full Monitor (SUPER+SHIFT+Print)
1. Press `SUPER+SHIFT+Print`
2. Expected: Current monitor captured, saved + copied
### Test 4: Annotate in Satty (SUPER+ALT+Print)
1. Press `SUPER+ALT+Print`
2. Drag to select a region
3. Expected: Satty window opens with the screenshot
4. Try these Satty actions:
- Freehand drawing (left toolbar)
- Rectangle/Arrow annotations
- Blur/redact sections
- Copy annotated result to clipboard
- Save annotated result to file
5. Exit Satty with `Esc` or `Ctrl+Q`
## Manual CLI Commands
If keybindings fail, test tools directly:
```bash
# Capture area and save to specific file
grim -g "$(slurp)" ~/Pictures/test-area.png
# Capture area and pipe to satty for editing
grim -g "$(slurp)" - | satty -f -
# Capture full screen and pipe to satty
grim - | satty -f -
# Copy area screenshot to clipboard
grim -g "$(slurp)" - | wl-copy
# grimblast manual modes
grimblast copysave area # area
grimblast copysave active # current window
grimblast copysave output # current monitor
grimblast save area ~/Pictures/test-grimblast.png # save only
```
## Satty Editor Shortcuts
| Action | Shortcut |
|--------|----------|
| Freehand | `F` |
| Rectangle | `R` |
| Ellipse | `E` |
| Line/Arrow | `L` |
| Text | `T` |
| Blur | `B` |
| Crop | `C` |
| Undo | `Ctrl+Z` |
| Redo | `Ctrl+Shift+Z` |
| Copy to clipboard | `Ctrl+C` |
| Save to file | `Ctrl+S` |
| Save As | `Ctrl+Shift+S` |
| Quit | `Esc` or `Ctrl+Q` |
## Dependencies
| Tool | Purpose | Status |
|------|---------|--------|
| grim | Wayland screenshot capture | Installed |
| slurp | Region/window selection | Installed |
| wl-clipboard | Clipboard support | Installed |
| satty | Annotation editor | Installed |
| grimblast | Hyprland screenshot helper | Installed |
| hyprshot | Alternative screenshot tool | Installed (not used in keybinds) |
## Troubleshooting
- **Print key not working**: Verify binding with `hyprctl binds | grep Print`
- **satty won't open**: Check `which satty` and `echo $XDG_SESSION_TYPE` (must be wayland)
- **slurp selector invisible**: Check if `grim` and `slurp` are in PATH
- **Screenshots not saving**: Check `~/Pictures/` directory exists
- **Clipboard empty**: Verify `wl-copy` and `wl-paste` work (`echo test | wl-copy && wl-paste`)
+2 -2
View File
@@ -105,8 +105,8 @@ last_updated: '2026-04-02'
| PCI Address | 08:00.0 |
> **Driver strategy**: This Pascal-generation GPU is being migrated from the
> pinned Manjaro `*-nvidia-575xx` branch to the mainline `*-nvidia` 595.71
> branch as part of the Hyprland/Wayland preparation. The migration is
> pinned Manjaro `*-nvidia-575xx` branch to `*-nvidia-580xx` (NVIDIA Fast Branch)
> as part of the Hyprland/Wayland preparation. The migration is
> automated by the `system-upgrade` Ansible role and documented in
> [ADR 0002 — Kernel and NVIDIA Driver Strategy for Hyprland/Wayland](../adr/0002-kernel-and-nvidia-strategy.md).
+126
View File
@@ -0,0 +1,126 @@
# Hyprland Lua Config Migration (0.55+)
## Summary
Hyprland 0.55 deprecates the legacy `hyprlang` (`.conf`) format in favor of a native Lua
DSL (`hyprland.lua`). Both formats coexist during transition, but `.lua` is the future.
## Configuration File Priority
| File | Format | Priority |
|------|--------|----------|
| `~/.config/hypr/hyprland.lua` | Lua DSL (`hl.*` API) | **Preferred** (0.55+) |
| `~/.config/hypr/hyprland.conf` | Legacy hyprlang | Deprecated |
When both exist, Hyprland loads `.lua` first. If `.lua` is present, `.conf` is ignored.
## Lua API Quick Reference
```lua
-- Variables
local terminal = "kitty"
local mainMod = "SUPER"
-- Config blocks
hl.config({ general = { gaps_in = 5 } })
hl.env("XCURSOR_SIZE", "24")
hl.monitor({ output = "", mode = "preferred", position = "auto", scale = "auto" })
-- Keybindings (single combined key string)
hl.bind(mainMod .. " + Q", hl.dsp.window.close())
hl.bind(mainMod .. " + T", hl.dsp.exec_cmd(terminal))
-- Window/workspace rules
hl.window_rule({ name = "my-rule", match = { class = ".*" }, suppress_event = "maximize" })
hl.workspace_rule({ workspace = 1, monitor = "DP-1", default = true })
-- Autostart
hl.on("hyprland.start", function() hl.exec_cmd("waybar &") end)
-- Gestures, animations
hl.gesture({ fingers = 3, direction = "horizontal", action = "workspace" })
hl.curve("easeOutQuint", { type = "bezier", points = { {0.23, 1}, {0.32, 1} } })
hl.animation({ leaf = "windows", enabled = true, speed = 4.79, spring = "easy" })
```
## Dispatcher Functions (`hl.dsp.*`)
| Lua API | Legacy .conf equivalent |
|---------|------------------------|
| `hl.dsp.exec_cmd("cmd")` | `exec, cmd` |
| `hl.dsp.window.close()` | `killactive` |
| `hl.dsp.window.float({ action = "toggle" })` | `togglefloating` |
| `hl.dsp.window.pseudo()` | `pseudo` |
| `hl.dsp.window.fullscreen({ mode = 0 })` | `fullscreen, 0` |
| `hl.dsp.window.move({ direction = "left" })` | `movewindow, l` |
| `hl.dsp.window.move({ workspace = 2 })` | `movetoworkspace, 2` |
| `hl.dsp.window.drag()` / `.resize()` | mouse bind |
| `hl.dsp.focus({ direction = "left" })` | `movefocus, l` |
| `hl.dsp.focus({ workspace = 2 })` | `workspace, 2` |
| `hl.dsp.focus({ workspace = "e+1" })` | `workspace, e+1` |
| `hl.dsp.workspace.toggle_special("magic")` | `togglespecialworkspace` |
| `hl.dsp.layout("togglesplit")` | `layoutmsg, togglesplit` |
## Pitfalls Learned
| Issue | Fix |
|-------|-----|
| `hl.dsp.focus({ direction = "next" })` | **Invalid.** Only `left/right/up/down` accepted. Use `hl.dsp.exec_cmd("hyprctl dispatch cyclenext")` instead. |
| `tap-to-click = true` | **Invalid Lua.** Hyphens not allowed in identifiers. Use `tap_to_click` (Hyprland accepts underscores). |
| `hl.bind("", "Print", ...)` | **Invalid.** Lua API uses single key string: `hl.bind("Print", ...)`. |
| `hl.bind(mainMod, "Q", ...)` | **Invalid.** Combine into one string: `hl.bind(mainMod .. " + Q", ...)`. |
| `resizeactive` not in `hl.dsp.*` | Use `hl.dsp.exec_cmd("hyprctl dispatch resizeactive -40 0")` as wrapper. |
| `luac -p` false positives | Standard `luac` can't validate Hyprland's custom `hl.*` API. Use `hyprctl configerrors` at runtime instead. |
## Validation
```bash
# Runtime validation (requires running Hyprland session)
hyprctl configerrors
# Over SSH (find instance signature first)
HYPRLAND_INSTANCE_SIGNATURE=$(ls /run/user/$(id -u)/hypr/) hyprctl configerrors
# Syntax-only check (limited - won't catch hl.* API issues)
luac -p ~/.config/hypr/hyprland.lua
```
## conf-to-lua Migration Mapping
| Legacy `.conf` | Lua `.lua` |
|----------------|------------|
| `$mainMod = SUPER` | `local mainMod = "SUPER"` |
| `monitor=DP-1,...` | `hl.monitor({ output = "DP-1", ... })` |
| `env = KEY,VALUE` | `hl.env("KEY", "VALUE")` |
| `input { ... }` | `hl.config({ input = { ... } })` |
| `general { ... }` | `hl.config({ general = { ... } })` |
| `bind = MOD, KEY, exec, cmd` | `hl.bind(mod .. " + KEY", hl.dsp.exec_cmd("cmd"))` |
| `bind = MOD, KEY, killactive` | `hl.bind(mod .. " + KEY", hl.dsp.window.close())` |
| `bind = MOD, KEY, togglefloating` | `hl.bind(mod .. " + KEY", hl.dsp.window.float({ action = "toggle" }))` |
| `bind = MOD, KEY, fullscreen, 0` | `hl.bind(mod .. " + KEY", hl.dsp.window.fullscreen({ mode = 0 }))` |
| `bind = MOD, KEY, movefocus, l` | `hl.bind(mod .. " + KEY", hl.dsp.focus({ direction = "left" }))` |
| `bind = MOD, KEY, movewindow, l` | `hl.bind(mod .. " + KEY", hl.dsp.window.move({ direction = "left" }))` |
| `bind = MOD, KEY, workspace, N` | `hl.bind(mod .. " + KEY", hl.dsp.focus({ workspace = N }))` |
| `bind = MOD, KEY, cyclenext` | `hl.bind(mod .. " + KEY", hl.dsp.exec_cmd("hyprctl dispatch cyclenext"))` |
| `bind = MOD, KEY, pseudo` | `hl.bind(mod .. " + KEY", hl.dsp.window.pseudo())` |
| `bind = MOD, KEY, layoutmsg, togglesplit` | `hl.bind(mod .. " + KEY", hl.dsp.layout("togglesplit"))` |
| `bind = , Print, exec, cmd` | `hl.bind("Print", hl.dsp.exec_cmd("cmd"))` |
| `bind = MOD, KEY, exit` | `hl.bind(mod .. " + KEY", hl.dsp.exec_cmd("hyprctl dispatch exit"))` |
| `device { name = "..."; ... }` | `hl.device({ name = "...", ... })` |
| `workspace = 1, monitor:DP-1, default:true` | `hl.workspace_rule({ workspace = 1, monitor = "DP-1", default = true })` |
| `exec-once = cmd` | `hl.on("hyprland.start", function() hl.exec_cmd("cmd") end)` |
| `col.active_border = rgba(...) 45deg` | `col = { active_border = { colors = {"rgba(...)"}, angle = 45 } }` |
## Multi-Machine Setup
This project manages configs for two machines with identical keybindings but different hardware:
| Aspect | Workstation (pfeddersheim) | MacBook Pro 16,1 |
|--------|---------------------------|-------------------|
| GPU | NVIDIA (6 env vars) | Intel UHD 630 + AMD RX 5500M (no env vars) |
| Monitors | 3 fixed (DP-1, DVI-D-1, HDMI-A-1) | Built-in + HDMI dock (auto-detect) |
| Input | de layout, left-handed mouse | de + mac_nodeadkeys + caps:swapescape, touchpad |
| Touchpad | N/A | natural_scroll, gestures, disable_while_typing |
| Media keys | N/A | Volume, brightness, playerctl |
| Window rules | None | Suppress maximize, XWayland fix |
| Config format | `.lua` (Ansible-managed) | `.lua` (manual) |
+7 -1
View File
@@ -6,7 +6,7 @@ tags:
- manual-overrides
- infrastructure
- configuration
last_updated: '2026-03-09'
last_updated: '2026-07-21'
---
# Manual System Overrides
@@ -22,4 +22,10 @@ This document tracks all persistent system changes implemented manually that are
| Date | Change | Rationale | Ansible Status |
|------|--------|-----------|----------------|
| 2026-07-21 | Added `agent0.localhost` (-> `reverse_proxy 192.168.0.141:8090`, `flush_interval -1`) in dedicated file `/etc/caddy/conf.d/agent0.localhost.conf`. First non-localhost backend (remote LAN host running a uvicorn app). Regenerated mkcert cert to add explicit `agent0.localhost` SAN; cert now has 20 SANs total, valid until 2028-10-21. Old cert/key backed up as `localhost{,-key}.pem.bak.<ts>`. Total `*.localhost` sites now 18. | User request to expose the remote agent backend on local HTTPS. Key learning: the `*.localhost` wildcard is NOT honored by curl/system trust even for single-label subdomains - `.localhost` is a special-use TLD (RFC 6761), so every subdomain needs an explicit SAN. This corrects the prior assumption that single-label subdomains were covered by the wildcard. | Same as parent Caddy row - candidate for Ansible role when issue #92 resolves |
| 2026-07-12 | Regenerated mkcert cert at `/etc/caddy/localhost{,-key}.pem` to add explicit SANs for `platform.localhost`, `packages.localhost`, and `onlyoffice.docs.localhost` (the latter was pre-existing in Caddy config but missing from the cert, causing TLS validation failures). Cert now has 19 SANs total, valid until 2028-10-12. | `onlyoffice.docs.localhost` is a multi-label subdomain not covered by `*.localhost` wildcard. Required explicit SAN. | Same as parent Caddy row |
| 2026-07-12 | Added `platform.localhost` (-> `localhost:10080`) and `packages.localhost` (-> `localhost:20080`) to `/etc/caddy/conf.d/localhost-dev.conf`. Docker port mappings: `~/internal/amicron-platform/docker-compose.override.yml` `10080:80`, `~/internal/satis/docker-compose.yml` nginx `20080:80`. Both upstreams are plain HTTP on high ports; Caddy terminates TLS. Also documented `onlyoffice.docs.localhost` (pre-existing in config, was undocumented). Total sites now 17. | Local HTTPS dev for amicron-platform (ERP), satis (Composer package repository), and shared Firebird 3.0.14 database. The three form the local E2E test triangle: `platform.localhost` (ERP) -> `packages.localhost` (Satis) -> Firebird (token validation). Mirrors the production `platform.auc.de` + `packages.auc.de` + shared Firebird setup. | Same as parent Caddy row - candidate for Ansible role when issue #92 resolves |
| 2026-07-06 | Caddy reverse-proxy + mkcert TLS at `/etc/caddy/` (`Caddyfile` + `conf.d/localhost-dev.conf` + `localhost{,-key}.pem`) | Local HTTPS dev for 14 `*.localhost` sites (mcp, artifacts, onlyoffice, demo.satware.com clone, 10 placeholders). Caddy runs as systemd `caddy.service` on port 443. NOT managed by this repo - no `caddy` Ansible role exists. The `onlyoffice.localhost` site (-> `localhost:3847`) is emitting `context canceled` SSE aborts; see `docs/learnings/2026-07-06-caddy-onlyoffice-mcp-sse-investigation.md` and GitLab issue [satware/mcp/onlyoffice#92](https://gitlab.satware.com/satware/mcp/onlyoffice/-/work_items/92). | Candidate for Ansible role (`caddy` + `mkcert`); defer until issue #92 resolves to avoid masking root cause |
| 2026-07-03 | Patched `remmina-plugin-rdp.so` in `~/.config/remmina/plugins/` | Remmina 1.4.43 crashes (SIGSEGV) on Wayland when `keymap` is set in a profile. Built from `~/external/remmina/` fork with NULL check fix. Alt plugin dir loads before system plugin. MR !2757 submitted upstream. See `docs/learnings/2026-07-03-remmina-wayland-crash-fix.md`. | Remove when upstream fix is released and packaged |
| 2026-06-17 | Discovered and deleted `br_vm_internal` bridge (10.10.2.0/24) | Externally-created bridge, no NM config file, no VMs attached. Origin unknown — likely ad-hoc `nmcli con add` or manual `brctl` session (Jun 10). Traffic-shaping qdisc (`htb`) suggests prior VM network isolation attempt. Deleted via `nmcli con delete` + `ip link delete`. | N/A (removed) |
| 2026-04-02 | Moved LLM models to `/home/mw/models/` (SATA SSD) with symlinks | Free root NVMe space (93%→87%, +25G freed) | Not yet |
+22
View File
@@ -0,0 +1,22 @@
# Memory — mw-pfeddersheim-workstation
> **Status**: 64 GiB migrated to `homeoffice` on 2026-07-13.
## Current Configuration
4x 8 GiB DDR4-3000 DIMMs = **32 GiB total**, dual-channel (A/B).
| Slot | Size | Type | Speed | Channel |
|------|------|------|-------|---------|
| DIMM_A1 | 8 GiB | DDR4 SDRAM | 3000 MT/s | BANK 0 |
| DIMM_A2 | 8 GiB | DDR4 UDIMM | 3000 MT/s | BANK 1 |
| DIMM_B1 | 8 GiB | DDR4 UDIMM | 3000 MT/s | BANK 2 |
| DIMM_B2 | 8 GiB | DDR4 UDIMM | 3000 MT/s | BANK 3 |
Note: DIMM_A1 reports "Unknown" form factor — different brand from the other three DIMMs.
## Migration History
| Date | Change |
|------|--------|
| 2026-07-13 | Removed 4x 16 GiB DDR4-2400 DIMMs (64 GiB) and transferred to `homeoffice`. Installed 4x 8 GiB DDR4-3000 DIMMs taken from `homeoffice` in exchange. |
+4 -4
View File
@@ -5,7 +5,7 @@ tags:
- setup
- bootstrap
- ansible
last_updated: '2026-05-20'
last_updated: '2026-06-17'
---
# Workstation Setup Guide
@@ -44,8 +44,8 @@ This guide describes how to bootstrap the **mw-pfeddersheim-workstation** infras
4. **Run system upgrade & kernel install** (preparation for Hyprland/Wayland)
This step runs the dedicated `system-upgrade` role: full `pacman -Syu`,
dual-kernel install (`linux612` LTS + `linux70` stable), and NVIDIA
driver migration from the pinned `575xx` branch to mainline `595.71`.
dual-kernel install (`linux618` LTS + `linux70` stable), and NVIDIA
driver migration from the pinned `575xx` branch to `580xx` (NVIDIA Fast Branch).
Always preview first with `--check`. See
[ADR 0002](../adr/0002-kernel-and-nvidia-strategy.md) for the rationale.
@@ -63,7 +63,7 @@ This guide describes how to bootstrap the **mw-pfeddersheim-workstation** infras
After it finishes, **reboot and select `linux70` from the GRUB menu**
before continuing to step 5. Verify with `uname -r` (expect 7.0.x)
and `nvidia-smi` (expect driver 595.71.05).
and `nvidia-smi` (expect driver 580.x).
5. **Run the Playbook** (remaining roles: common, dev-tools, maintenance)
```bash
+48 -1
View File
@@ -6,7 +6,7 @@ tags:
- software-stack
- inventory
- tech-stack
last_updated: '2026-05-22'
last_updated: '2026-07-12'
---
# Software Stack - mw-pfeddersheim-workstation
@@ -92,3 +92,50 @@ Packages previously listed as "Misc" reclassified to functional domains:
- **Ansible**: `/usr/bin/ansible`
- **Python**: `/usr/bin/python`
- **Docker**: `/usr/bin/docker`
## Local Dev Services (Manual - not Ansible-managed)
System-level services running on this workstation that are configured manually
outside this repo's Ansible roles. See `docs/tech/manual-overrides.md` for the
full override table and `docs/learnings/2026-07-06-caddy-onlyoffice-mcp-sse-investigation.md`
for the active Caddy/OnlyOffice investigation.
### Caddy (port 443, systemd `caddy.service`)
| Item | Value |
|------|-------|
| Package | `caddy` (pacman) |
| Service | `caddy.service` (enabled, runs as `caddy` user) |
| Config | `/etc/caddy/Caddyfile` + `/etc/caddy/conf.d/*.conf` (main site file `localhost-dev.conf`, plus per-service files like `agent0.localhost.conf`) |
| TLS cert | mkcert-generated pair at `/etc/caddy/localhost{,-key}.pem`; includes explicit SANs for all 20 hostnames, valid until 2028-10-21. The `*.localhost` wildcard is NOT honored by curl/system trust even for single-label subdomains like `agent0.localhost` (empirically verified 2026-07-21: `.localhost` is a special-use TLD per RFC 6761); every subdomain MUST be listed explicitly. Multi-label hosts like `onlyoffice.docs.localhost` additionally require explicit SANs because wildcards only cover one level. |
| Admin endpoint | Unix socket `/run/caddy/admin.socket` |
| Sites | 18 `*.localhost` virtual hosts (see table below) |
#### `*.localhost` sites
| Site | Backend / Handler | Notes |
|------|-------------------|-------|
| `mcp.localhost` | `reverse_proxy localhost:50880` | MCP server (node, pid 1167) |
| `artifacts.localhost` | `file_server browse` root `/var/www/artifacts` | Static artifacts |
| `onlyoffice.localhost` | `reverse_proxy localhost:3847` | OnlyOffice MCP server; emitting `context canceled` SSE aborts (issue [#92](https://gitlab.satware.com/satware/mcp/onlyoffice/-/work_items/92)) |
| `project-1.localhost` ... `project-10.localhost` | `respond "project-N - placeholder"` | 10 reserved placeholders |
| `demo.satware.com.localhost` | `reverse_proxy localhost:38081` | webdevops/php-apache-dev container |
| `platform.localhost` | `reverse_proxy localhost:10080` | amicron-platform Docker (Symfony 7.4 + Vue 3 + Firebird 3.0.14); `~/internal/amicron-platform` |
| `packages.localhost` | `reverse_proxy localhost:20080` | satis Docker (Composer package repository with token-based access control); `~/internal/satis` |
| `onlyoffice.docs.localhost` | `file_server` root `/home/mw/internal/onlyoffice/docs-site/dist` | Starlight static docs build |
| `agent0.localhost` | `reverse_proxy 192.168.0.141:8090` | First non-localhost backend (remote host on LAN, uvicorn app). Config in dedicated `/etc/caddy/conf.d/agent0.localhost.conf`. `flush_interval -1` enabled. |
Wildcard `*.localhost` only matches single-label subdomains (e.g. `mcp.localhost`)
AND is not honored by curl/system trust for the `.localhost` special-use TLD
(RFC 6761) - every subdomain needs an explicit SAN. Multi-label hosts like
`onlyoffice.docs.localhost` and `demo.satware.com.localhost` require explicit
SANs for both reasons.
### Useful Caddy commands
```bash
sudo caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
sudo systemctl reload caddy
sudo journalctl -u caddy --no-pager -n 50
curl -sk https://onlyoffice.localhost/ping # test (skips cert verify)
```
+6 -2
View File
@@ -7,7 +7,7 @@ tags:
- health-check
- workstation
- monitoring
last_updated: '2026-03-17'
last_updated: '2026-06-17'
---
# Workstation Health Report: mw-manjaro-pf
@@ -87,4 +87,8 @@ Uptime: ~1 hour, 7 minutes
## Next Steps
1. Consider setting up automated crash monitoring for high-memory applications
2. Run health check script manually as needed: `/home/mw/internal/mw-pfeddersheim-workstation/scripts/morning-health-check.sh` (automated systemd timer removed 2026-05-26)
2. Run health check script manually as needed: `/home/mw/internal/mw-pfeddersheim-workstation/scripts/morning-health-check.sh` (automated systemd timer removed 2026-05-26)
## Recent Cleanup (2026-06-17)
- **Externally-created bridge `br_vm_internal` (10.10.2.0/24)** discovered and removed. No NM config file, no libvirt network, no attached VMs. Origin traced to Jun 10 2026 (first activation) — likely ad-hoc CLI creation. Bridge had `htb` qdisc suggesting prior VM network isolation attempt. Both the NM connection and kernel interface fully deleted.
-167
View File
@@ -1,167 +0,0 @@
---
title: "Yazi v26.5.6 Cheatsheet"
author: "mw-pfeddersheim-workstation"
date: "May 2026"
---
# Yazi Cheatsheet
**Version:** 26.5.6 (Arch Linux 2026-05-10) **Config:** `~/.config/yazi/`
## Quick Start
| Key | Action |
|-----|--------|
| `yazi` | Launch Yazi |
| `y` | Shell wrapper (cd persists on exit) |
| `q` | Quit normal (apply cwd) |
| `Q` | Quit without cwd change |
| `F1` | Show help |
## Navigation
| Key | Action |
|-----|--------|
| `h` | Parent directory |
| `l` | Enter dir / open file |
| `j` / `k` | Down / Up |
| `H` / `L` | History back / forward |
| `Enter` | Open hovered file |
| `Tab` | Toggle file details |
| `.` | Toggle hidden files |
| `~` | Home directory (DE: Shift+^+Space) |
| `g g` | Go to top |
| `G` | Go to bottom |
## Search
| Key | Action |
|-----|--------|
| `/` | Search forward |
| `?` | Search backward |
| `n` / `N` | Next / previous match |
| `f` | Interactive filter (fuzzy) |
| `S` | Search with fd |
## File Operations
| Key | Action |
|-----|--------|
| `Space` | Toggle selection |
| `v` | Visual mode range select |
| `Ctrl+A` | Select all |
| `Esc` | Clear selection |
| `y` | Yank (copy) |
| `p` | Paste |
| `X` | Cut |
| `x` | Undo cut |
| `D` | Delete to trash |
| `Shift+D` | Permanent delete |
| `a` | New file |
| `Shift+A` | New directory |
| `R` | Rename |
| `Shift+R` | Bulk rename |
| `O` | "Open with..." menu |
## Sorting (comma prefix)
| Key | Action |
|-----|--------|
| `, m` | Modified (newest) |
| `, M` | Modified reverse |
| `, c` | Created (newest) |
| `, C` | Created reverse |
| `, e` | Extension (alpha) |
| `, a` | Alphabetic |
| `, s` | Size (largest) |
| `, S` | Size (smallest, reverse) |
## Tabs
| Key | Action |
|-----|--------|
| `t` | New tab |
| `1` ... `9` | Switch to tab |
| `[` / `]` | Prev / next tab |
| `W` | Close tab |
## Shell & Commands
| Key | Action |
|-----|--------|
| `!` | Drop into shell here |
| `:` | Command line |
| `Ctrl+S` | Run shell command |
## Emit Commands (`:` prefix)
| Command | Action |
|---------|--------|
| `:cd <path>` | Change directory |
| `:reveal` | Reveal file in parent dir |
| `:tab_create` | Open new tab |
| `:tab_switch N` | Switch to tab N |
| `:quit` | Quit Yazi |
| `:yank` | Yank selected |
| `:paste` | Paste yanked |
| `:remove` | Trash |
| `:rename` | Rename hovered |
| `:shell <cmd>` | Run shell command |
## Power Tricks
### 1. Shell Wrapper (cd persists)
Use the `y` wrapper instead of raw `yazi` so your shell cds into the directory you navigated to on exit.
### 2. Clipboard on Yank (Wayland)
Bind `y` in `keymap.toml` to `wl-copy -t text/uri-list` then yank for system clipboard integration.
### 3. Git Root Jump
Bind `g r` to `ya emit cd "$(git rev-parse --show-toplevel)"`
### 4. Escape Closes Input
In `keymap.toml` input section: map `Esc` to `close` (skip vim mode).
### 5. Disable Image Previews
In `yazi.toml` `plugin` section: `preloaders = []` and `previewers = []`.
For network mounts: `url = /remote/** run = noop`.
### 6. Hostname in Header
In `init.lua`: `Header:children_add` with `user_name()` + `host_name()` spans.
### 7. Show Symlink Target in Status Bar
In `init.lua`: `Status:children_add` with `h.link_to`.
## Configuration Files
| File | Purpose |
|------|---------|
| `keymap.toml` | Custom key bindings |
| `yazi.toml` | Behavior, preview rules, openers |
| `theme.toml` | Colors and UI theme |
| `init.lua` | Lua startup script |
| `plugins/` | Plugin directory |
## Recommended Plugins
| Plugin | Purpose |
|--------|---------|
| `smart-enter` | One key: open file or enter dir |
| `smart-paste` | Paste without entering dir |
| `smart-tab` | New tab + enter hovered dir |
| `toggle-pane` | Maximize / hide preview pane |
| `full-border` | Full panel borders |
| `confirm-quit` | Confirm on multi-tab |
| `zoxide` | Zoxide dir jumping |
| `chmod` | Change file permissions |
## Daily Drill
| Area | Keys |
|------|------|
| **Nav** | `j/k` move / `l` enter / `h` back |
| **Files** | `Space` select / `y` copy / `p` paste / `X` cut |
| **Delete** | `D` trash / `R` rename / `a` create |
| **Search** | `/` search / `f` filter / `g` top / `G` bottom |
| **Tabs** | `t` new / `1-9` switch / `W` close |
| **Misc** | `!` shell / `q` quit / `F1` help |
+372
View File
@@ -0,0 +1,372 @@
#!/bin/bash
set -euo pipefail
# Workstation Package Cleanup Script
# Version: 1.0.0
# Target: mw-pfeddersheim-workstation
# Purpose: Remove identified cleanup candidates and log for verification
MODE="apply"
if [[ "${1:-}" == "--check" ]]; then
MODE="check"
echo "=== Running in CHECK mode (dry-run, no packages removed) ==="
elif [[ "${1:-}" == "--help" ]]; then
echo "Usage: $0 [--check | --apply | --help]"
echo " --check Dry-run: show what would be removed (default)"
echo " --apply Actually remove packages"
echo " --help Show this help"
exit 0
fi
# Ensure log directory exists
mkdir -p "$HOME/logs" 2>/dev/null || true
LOCAL_LOG="$HOME/logs/cleanup-$(date +%Y%m%d-%H%M%S).log"
BOLD=$(tput bold 2>/dev/null || true)
RED=$(tput setaf 1 2>/dev/null || true)
GREEN=$(tput setaf 2 2>/dev/null || true)
RESET=$(tput sgr0 2>/dev/null || true)
log() { echo -e "$*" | tee -a "$LOCAL_LOG"; }
section() { log ""; log "${BOLD}--- $1 ---${RESET}"; }
echo "" | tee -a "$LOCAL_LOG"
log "============================================="
log " Workstation Package Cleanup"
log " Mode: $MODE | Date: $(date)"
log " Log: $LOCAL_LOG"
log "============================================="
# Track totals
TOTAL_CANDIDATES=0
TOTAL_REMOVED=0
TOTAL_ERRORS=0
SKIPPED=0
# Helper: add package to candidate list file
add_candidates() {
local category="$1"
shift
echo "$@" >> "$HOME/logs/.cleanup-candidates-${category}.txt"
}
# ============================================================
# 1. PRE-FLIGHT CHECKS
# ============================================================
section "Pre-flight Checks"
# Verify pacman is available
if ! command -v pacman &> /dev/null; then
log "${RED}ERROR: pacman not found. This script must be run on Arch/Manjaro.${RESET}"
exit 1
fi
# Check disk space (need room for transaction)
DISK_PCT=$(df / | awk 'NR==2 {gsub(/%/,"",$5); print $5}')
if [[ "$DISK_PCT" -gt 95 ]]; then
log "${RED}WARNING: Disk usage at ${DISK_PCT}%. Consider freeing space first.${RESET}"
fi
DISK_AVAIL=$(df -h / | awk 'NR==2 {print $4}')
log "Disk available: $DISK_AVAIL"
# Check for existing packages (only list ones that are installed)
log "Scanning for installed cleanup candidates..."
# ============================================================
# 2. OLD MULTIMEDIA PACKAGES
# ============================================================
section "[1/7] Old Multimedia Packages"
CANDIDATES=()
# Old ffmpeg version
if pacman -Qe ffmpeg4.4 &>/dev/null || pacman -Q ffmpeg4.4 &>/dev/null; then
CANDIDATES+=(ffmpeg4.4)
fi
# VLC plugins (keep base vlc)
while IFS= read -r pkg; do
[[ "$pkg" == "vlc" ]] && continue # skip base vlc
CANDIDATES+=("$pkg")
done < <(pacman -Qq 2>/dev/null | grep -E '^vlc-' || true)
TOTAL_CANDIDATES=$((TOTAL_CANDIDATES + ${#CANDIDATES[@]}))
log "Found ${#CANDIDATES[@]} multimedia candidates: ${CANDIDATES[*]:0:5}..."
if [[ ${#CANDIDATES[@]} -gt 0 ]]; then
log "Candidates: ${CANDIDATES[*]}"
if [[ "$MODE" == "check" ]]; then
log "DRY-RUN: Would run: sudo pacman -Rns --noconfirm ${CANDIDATES[*]}"
else
sudo pacman -Rns --noconfirm "${CANDIDATES[@]}" 2>&1 | tee -a "$LOCAL_LOG" || true
TOTAL_REMOVED=$((TOTAL_REMOVED + ${#CANDIDATES[@]}))
fi
fi
# ============================================================
# 3. INACTIVE NVIDIA/MHWHD DRIVERS
# ============================================================
section "[2/7] Inactive NVIDIA Drivers (mhwd-nvidia-*)"
CANDIDATES=()
while IFS= read -r pkg; do
[[ -n "$pkg" ]] && CANDIDATES+=("$pkg")
done < <(pacman -Qq 2>/dev/null | grep -E '^mhwd-nvidia-' || true)
TOTAL_CANDIDATES=$((TOTAL_CANDIDATES + ${#CANDIDATES[@]}))
if [[ ${#CANDIDATES[@]} -gt 0 ]]; then
log "Found ${#CANDIDATES[@]} mhwd-nvidia candidates: ${CANDIDATES[*]}"
if [[ "$MODE" == "check" ]]; then
log "DRY-RUN: Would run: sudo pacman -Rns --noconfirm ${CANDIDATES[*]}"
else
sudo pacman -Rns --noconfirm "${CANDIDATES[@]}" 2>&1 | tee -a "$LOCAL_LOG" || true
TOTAL_REMOVED=$((TOTAL_REMOVED + ${#CANDIDATES[@]}))
fi
else
log "No mhwd-nvidia packages found."
fi
# ============================================================
# 4. OLD KERNELS
# ============================================================
section "[3/7] Old Kernel Packages"
CANDIDATES=()
# linux618 and linux70 (keep current running kernel and its modules)
CURRENT_KERNEL=$(uname -r)
KMVR="${CURRENT_KERNEL%-MANJARO}" # 6.18.33-1
KMJ="${KMVR%%.*}" # 6
KMREST="${KMVR#*.}" # 18.33-1
KMN="${KMREST%%.*}" # 18
CURRENT_KERNEL_PKG="linux${KMJ}${KMN}" # linux618
log "Current running kernel: $CURRENT_KERNEL (package: $CURRENT_KERNEL_PKG)"
for kernel in linux618 linux70; do
# Skip if this is the running kernel package
if [[ "$kernel" == "linux${CURRENT_KERNEL_PKG}" ]]; then
log "Skipping $kernel — this is the running kernel."
continue
fi
if pacman -Q "$kernel" &>/dev/null; then
CANDIDATES+=("$kernel")
# Also remove its modules/headers/etc (but not nvidia/r8168 for running kernel — already skipped above)
while IFS= read -r pkg; do
[[ -n "$pkg" ]] && CANDIDATES+=("$pkg")
done < <(pacman -Qq 2>/dev/null | grep -E "^${kernel}-" || true)
fi
done
TOTAL_CANDIDATES=$((TOTAL_CANDIDATES + ${#CANDIDATES[@]}))
if [[ ${#CANDIDATES[@]} -gt 0 ]]; then
log "Found ${#CANDIDATES[@]} kernel candidates: ${CANDIDATES[*]}"
if [[ "$MODE" == "check" ]]; then
log "DRY-RUN: Would run: sudo pacman -Rns --noconfirm ${CANDIDATES[*]}"
else
sudo pacman -Rns --noconfirm "${CANDIDATES[@]}" 2>&1 | tee -a "$LOCAL_LOG" || true
TOTAL_REMOVED=$((TOTAL_REMOVED + ${#CANDIDATES[@]}))
fi
else
log "No old kernel packages found."
fi
# ============================================================
# 5. lib32 PACKAGES
# ============================================================
section "[4/7] lib32 Packages"
CANDIDATES=()
while IFS= read -r pkg; do
[[ -n "$pkg" ]] && CANDIDATES+=("$pkg")
done < <(pacman -Qq 2>/dev/null | grep -E '^lib32-' || true)
TOTAL_CANDIDATES=$((TOTAL_CANDIDATES + ${#CANDIDATES[@]}))
log "Found ${#CANDIDATES[@]} lib32 packages"
if [[ ${#CANDIDATES[@]} -gt 0 ]]; then
# List first 10, then total
log "First 10 candidates: ${CANDIDATES[*]:0:10}..."
log "Total: ${#CANDIDATES[@]} lib32 packages"
# Interactive: ask user if they want to remove lib32 packages
if [[ "$MODE" == "check" ]]; then
log "DRY-RUN: Would run: sudo pacman -Rns --noconfirm ${CANDIDATES[*]}"
else
log "${BOLD}This will remove ALL ${#CANDIDATES[@]} lib32 packages.${RESET}"
log "Only remove if you don't run 32-bit applications (games, wine, etc.)"
read -rp "Remove all lib32 packages? [y/N] " confirm
if [[ "$confirm" =~ ^[Yy]$ ]]; then
sudo pacman -Rns --noconfirm "${CANDIDATES[@]}" 2>&1 | tee -a "$LOCAL_LOG" || true
TOTAL_REMOVED=$((TOTAL_REMOVED + ${#CANDIDATES[@]}))
else
log "SKIPPED lib32 removal."
SKIPPED=$((SKIPPED + ${#CANDIDATES[@]}))
fi
fi
fi
# ============================================================
# 6. REDUNDANT FONTS (Nerd Font variants)
# ============================================================
section "[5/7] Redundant Font Packages"
CANDIDATES=()
# Nerd Font packages (keep the one in active use)
while IFS= read -r pkg; do
[[ -n "$pkg" ]] && CANDIDATES+=("$pkg")
done < <(pacman -Qq 2>/dev/null | grep -iE 'nerd-font' || true)
# Also check for duplicate font collections
while IFS= read -r pkg; do
[[ -n "$pkg" ]] && CANDIDATES+=("$pkg")
done < <(pacman -Qq 2>/dev/null | grep -iE 'ttf-(jetbrains|iosevka|fira|hack|noto)' || true)
TOTAL_CANDIDATES=$((TOTAL_CANDIDATES + ${#CANDIDATES[@]}))
if [[ ${#CANDIDATES[@]} -gt 0 ]]; then
log "Found ${#CANDIDATES[@]} font candidates:"
for pkg in "${CANDIDATES[@]}"; do
log " - $pkg"
done
if [[ "$MODE" == "check" ]]; then
log "DRY-RUN: Would show above packages for manual review"
else
log "${RED}CRITICAL: Review font packages carefully - removing wrong fonts can break UI.${RESET}"
read -rp "Remove these ${#CANDIDATES[@]} font packages? [y/N] " confirm
if [[ "$confirm" =~ ^[Yy]$ ]]; then
sudo pacman -Rns --noconfirm "${CANDIDATES[@]}" 2>&1 | tee -a "$LOCAL_LOG" || true
TOTAL_REMOVED=$((TOTAL_REMOVED + ${#CANDIDATES[@]}))
else
log "SKIPPED font removal."
SKIPPED=$((SKIPPED + ${#CANDIDATES[@]}))
fi
fi
else
log "No redundant font packages found."
fi
# ============================================================
# 7. BROWSERS (keep current active, remove extras)
# ============================================================
section "[6/7] Browser Packages"
CANDIDATES=()
# Check which browsers are installed
BROWSERS=()
for browser in firefox chromium google-chrome firefox-developer-edition brave-bin microsoft-edge; do
if pacman -Q "$browser" &>/dev/null; then
BROWSERS+=("$browser")
fi
done
log "Installed browsers: ${BROWSERS[*]}"
log "${BOLD}Recommended: Keep firefox-developer-edition + one other${RESET}"
if [[ ${#BROWSERS[@]} -gt 2 ]]; then
# Suggest removal of non-essential browsers
SUGGESTED_REMOVAL=()
for b in "${BROWSERS[@]}"; do
# Keep firefox-developer-edition and chromium/google-chrome
if [[ "$b" != "firefox-developer-edition" && "$b" != "chromium" && "$b" != "google-chrome" ]]; then
SUGGESTED_REMOVAL+=("$b")
fi
done
if [[ ${#SUGGESTED_REMOVAL[@]} -gt 0 ]]; then
CANDIDATES=("${SUGGESTED_REMOVAL[@]}")
TOTAL_CANDIDATES=$((TOTAL_CANDIDATES + ${#CANDIDATES[@]}))
log "Suggested removal: ${CANDIDATES[*]}"
if [[ "$MODE" == "check" ]]; then
log "DRY-RUN: Would run: sudo pacman -Rns --noconfirm ${CANDIDATES[*]}"
else
sudo pacman -Rns --noconfirm "${CANDIDATES[@]}" 2>&1 | tee -a "$LOCAL_LOG" || true
TOTAL_REMOVED=$((TOTAL_REMOVED + ${#CANDIDATES[@]}))
fi
fi
else
log "Browser count is manageable (${#BROWSERS[@]}). No action needed."
fi
# ============================================================
# 8. UNUSED PYTHON PACKAGES
# ============================================================
section "[7/7] Unused Python GUI Packages"
CANDIDATES=()
PYTHON_CANDIDATES=(
wxpython
wxpython-gtk3
pyqt5
pyqt5-sip
pyqt6
pyqt6-sip
fluidity
tk
tkinter
python-tk
)
for pkg in "${PYTHON_CANDIDATES[@]}"; do
if pacman -Q "$pkg" &>/dev/null; then
CANDIDATES+=("$pkg")
fi
done
TOTAL_CANDIDATES=$((TOTAL_CANDIDATES + ${#CANDIDATES[@]}))
if [[ ${#CANDIDATES[@]} -gt 0 ]]; then
log "Found ${#CANDIDATES[@]} Python GUI candidates: ${CANDIDATES[*]}"
if [[ "$MODE" == "check" ]]; then
log "DRY-RUN: Would run: sudo pacman -Rns --noconfirm ${CANDIDATES[*]}"
else
sudo pacman -Rns --noconfirm "${CANDIDATES[@]}" 2>&1 | tee -a "$LOCAL_LOG" || true
TOTAL_REMOVED=$((TOTAL_REMOVED + ${#CANDIDATES[@]}))
fi
else
log "No unused Python GUI packages found."
fi
# ============================================================
# CLEANUP ORPHANS & CACHE
# ============================================================
section "Post-Cleanup: Orphans & Cache"
log "Finding orphaned dependencies..."
ORPHANS=$(pacman -Qtdq 2>/dev/null || true)
if [[ -n "$ORPHANS" ]]; then
ORPHAN_COUNT=$(echo "$ORPHANS" | wc -l)
log "Found $ORPHAN_COUNT orphaned packages"
if [[ "$MODE" == "check" ]]; then
log "DRY-RUN: Would run: sudo pacman -Rns --noconfirm $(echo $ORPHANS)"
else
sudo pacman -Rns --noconfirm $ORPHANS 2>&1 | tee -a "$LOCAL_LOG" || true
TOTAL_REMOVED=$((TOTAL_REMOVED + ORPHAN_COUNT))
fi
else
log "No orphaned packages found."
fi
log "Cleaning pacman cache (keep last 2 versions)..."
if [[ "$MODE" == "check" ]]; then
CACHE_SIZE=$(du -sh /var/cache/pacman/pkg/ 2>/dev/null | cut -f1)
log "Cache size: $CACHE_SIZE"
log "DRY-RUN: Would run: sudo paccache -rk2"
else
sudo paccache -rk2 2>&1 | tee -a "$LOCAL_LOG" || true
fi
# ============================================================
# SUMMARY
# ============================================================
section "Cleanup Summary"
FINAL_DISK=$(df -h / | awk 'NR==2 {print $4}')
log "Disk space available after cleanup: $FINAL_DISK"
log "Total candidates identified: $TOTAL_CANDIDATES"
log "Packages removed: $TOTAL_REMOVED"
log "Packages skipped: $SKIPPED"
log "Log saved to: $LOCAL_LOG"
log ""
log "============================================="
log " Cleanup Complete"
log "============================================="
# Cleanup temp files
rm -f "$HOME/logs/.cleanup-candidates-"*.txt 2>/dev/null || true
+230
View File
@@ -0,0 +1,230 @@
#!/bin/bash
set -euo pipefail
# NVIDIA Kernel Module Recovery Script
# Purpose: Reinstall nvidia-580xx-dkms and rebuild kernel module after package cleanup removed it
# Target: mw-pfeddersheim-workstation (Manjaro, NVIDIA GPU)
MODE="apply"
if [[ "${1:-}" == "--check" ]]; then
MODE="check"
echo "=== Running in CHECK mode (diagnostic only, no changes) ==="
elif [[ "${1:-}" == "--help" ]]; then
echo "Usage: $0 [--check | --apply | --help]"
echo " --check Diagnose and show what would be done"
echo " --apply Reinstall nvidia-580xx-dkms and rebuild module (requires sudo)"
echo " --help Show this help"
exit 0
fi
mkdir -p "$HOME/logs" 2>/dev/null || true
LOCAL_LOG="$HOME/logs/nvidia-recovery-$(date +%Y%m%d-%H%M%S).log"
BOLD=$(tput bold 2>/dev/null || true)
RED=$(tput setaf 1 2>/dev/null || true)
GREEN=$(tput setaf 2 2>/dev/null || true)
YELLOW=$(tput setaf 3 2>/dev/null || true)
RESET=$(tput sgr0 2>/dev/null || true)
log() { echo -e "$*" | tee -a "$LOCAL_LOG"; }
section() { log ""; log "${BOLD}--- $1 ---${RESET}"; }
echo "" | tee -a "$LOCAL_LOG"
log "============================================="
log " NVIDIA Kernel Module Recovery"
log " Date: $(date)"
log " Log: $LOCAL_LOG"
log "============================================="
# ============================================================
# 1. DIAGNOSTICS
# ============================================================
section "Diagnostic"
# Get current kernel
RUNNING_KERNEL=$(uname -r)
log "Running kernel: $RED$RUNNING_KERNEL$RESET"
KERNEL_VERSION="${RUNNING_KERNEL%-MANJARO}" # 6.18.33-1
KERNEL_MAJOR="${KERNEL_VERSION%%.*}" # 6
KERNEL_MINOR="${KERNEL_VERSION#*.}" # 18.33-1
KERNEL_MINOR="${KERNEL_MINOR%%.*}" # 18
KERNEL_PKG="linux${KERNEL_MAJOR}${KERNEL_MINOR}" # linux618
log "Kernel package: $KERNEL_PKG"
# Check if nvidia module exists
MODULE_PATH="/usr/lib/modules/$RUNNING_KERNEL/extra/modules/nvidia.ko.zst"
MODULE_PATH2="/usr/lib/modules/$RUNNING_KERNEL/extra/nvidia.ko.zst"
MODULE_PATH3="/usr/lib/modules/$RUNNING_KERNEL/updates/dkms/nvidia.ko.zst"
MODULE_PATH4="/usr/lib/modules/$RUNNING_KERNEL/updates/nvidia.ko.zst"
MODULE_PATH5="/usr/lib/modules/$RUNNING_KERNEL/kernel/drivers/gpu/drm/nvidia/nvidia.ko.zst"
MODULE_FOUND=false
if [[ -f "$MODULE_PATH" ]] || [[ -f "$MODULE_PATH2" ]] || [[ -f "$MODULE_PATH3" ]] || [[ -f "$MODULE_PATH4" ]] || [[ -f "$MODULE_PATH5" ]]; then
MODULE_FOUND=true
log "${GREEN}NVIDIA module found at $MODULE_PATH3$RESET"
else
log "${RED}NVIDIA module MISSING for kernel $RUNNING_KERNEL$RESET"
fi
# Check nvidia-utils
if command -v nvidia-smi &>/dev/null; then
log "${GREEN}nvidia-smi: available$RESET"
else
log "${RED}nvidia-smi: not found$RESET"
fi
# Check DKMS
if command -v dkms &>/dev/null; then
log "${GREEN}dkms: available$RESET"
else
log "${RED}dkms: not found$RESET"
fi
# Check if nvidia-580xx-dkms is installed
if pacman -Q nvidia-580xx-dkms &>/dev/null; then
DKMS_INSTALLED=true
log "${GREEN}nvidia-580xx-dkms: installed$(pacman -Qi nvidia-580xx-dkms 2>/dev/null | grep Version)"
else
DKMS_INSTALLED=false
log "${RED}nvidia-580xx-dkms: NOT installed$RESET"
fi
# Check if nvidia-utils is installed
if pacman -Q nvidia-utils &>/dev/null; then
log "${GREEN}nvidia-utils: installed"
else
log "${RED}nvidia-utils: NOT installed$RESET"
fi
# Check modules-load config
if [[ -f /etc/modules-load.d/mhwd-gpu.conf ]]; then
log "modules-load config: /etc/modules-load.d/mhwd-gpu.conf exists"
log " Entries: $(grep -v '^#\|^$' /etc/modules-load.d/mhwd-gpu.conf | tr '\n' ' ')"
else
log "${RED}No mhwd-gpu.conf found$RESET"
fi
# ============================================================
# 2. CHECK DEPENDENCIES
# ============================================================
section "Dependency Check"
MISSING=()
if ! command -v dkms &>/dev/null; then
MISSING+=(dkms)
fi
if ! command -v mkinitcpio &>/dev/null; then
MISSING+=(mkinitcpio)
fi
if [[ ${#MISSING[@]} -gt 0 ]]; then
log "${RED}Missing base tools: ${MISSING[*]}$RESET"
log "These will be installed as part of the fix."
fi
# Check available disk space
DISK_AVAIL=$(df -h / | awk 'NR==2 {print $4}')
log "Disk available: $DISK_AVAIL"
# ============================================================
# 3. APPLY FIX
# ============================================================
section "Applying Fix"
if [[ "$MODE" == "check" ]]; then
log "${BOLD}DRY-RUN MODE${RESET}"
log "Would execute:"
log " sudo pacman -Rns --noconfirm nvidia-utils # remove conflicting non-580xx driver"
log " sudo pacman -S --needed nvidia-580xx-dkms nvidia-580xx-utils"
log " sudo pacman -S --needed linux618-headers # kernel headers for DKMS"
log " sudo dkms autoinstall"
log " sudo mkinitcpio -P"
log " sudo reboot"
exit 0
fi
# Confirm before destructive action
log "${BOLD}This will reinstall the NVIDIA kernel module driver.$RESET"
log "The system will reboot automatically."
read -rp "Continue? [y/N] " confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
log "ABORTED by user."
exit 0
fi
# Step 1: Resolve driver conflict, install 580xx suite
log "[1/5] Resolving NVIDIA driver conflict and installing 580xx suite..."
# Check if newer nvidia-utils conflicts with 580xx-dkms
if pacman -Q nvidia-utils &>/dev/null && ! pacman -Q nvidia-580xx-utils &>/dev/null; then
log "Conflict: nvidia-utils (non-580xx) installed alongside nvidia-580xx-dkms."
log "Removing conflicting nvidia-utils, installing nvidia-580xx-utils..."
sudo pacman -Rns --noconfirm nvidia-utils 2>&1 | tee -a "$LOCAL_LOG"
sudo pacman -S --needed --noconfirm nvidia-580xx-dkms nvidia-580xx-utils 2>&1 | tee -a "$LOCAL_LOG"
else
sudo pacman -S --needed --noconfirm nvidia-580xx-dkms nvidia-580xx-utils 2>&1 | tee -a "$LOCAL_LOG"
fi
# Step 2: Ensure kernel headers are available for DKMS
log "[1b/4] Checking kernel headers for DKMS..."
KERNEL_HEADERS="${KERNEL_PKG}-headers"
if pacman -Q "$KERNEL_HEADERS" &>/dev/null; then
log "${GREEN}Kernel headers ($KERNEL_HEADERS) already installed.$RESET"
else
log "Installing $KERNEL_HEADERS for DKMS..."
sudo pacman -S --needed --noconfirm "$KERNEL_HEADERS" 2>&1 | tee -a "$LOCAL_LOG"
fi
# Step 3: Rebuild DKMS module for current kernel
log "[2/5] Rebuilding DKMS module for kernel $RUNNING_KERNEL..."
sudo dkms autoinstall 2>&1 | tee -a "$LOCAL_LOG"
# Step 3: Verify module was created
log "[3/5] Verifying module..."
if [[ -f "$MODULE_PATH" ]] || [[ -f "$MODULE_PATH2" ]] || [[ -f "$MODULE_PATH3" ]] || [[ -f "$MODULE_PATH4" ]] || [[ -f "$MODULE_PATH5" ]]; then
log "${GREEN}Module verified!$RESET"
# Show module details
modinfo_output=$(modinfo nvidia 2>/dev/null || echo "modinfo failed")
log " $modinfo_output"
else
log "${RED}WARNING: Module file not found after DKMS build.$RESET"
# Detect version for manual build
DKMS_VERSION=$(pacman -Qi nvidia-580xx-dkms 2>/dev/null | grep Version | awk '{print $2}' | cut -d- -f1)
[[ -z "$DKMS_VERSION" ]] && DKMS_VERSION="580.159.04"
log "Attempting manual build (version: $DKMS_VERSION)..."
sudo dkms build -m nvidia -v "$DKMS_VERSION" -k "$RUNNING_KERNEL" 2>&1 | tee -a "$LOCAL_LOG"
sudo dkms install -m nvidia -v "$DKMS_VERSION" -k "$RUNNING_KERNEL" 2>&1 | tee -a "$LOCAL_LOG"
fi
# Step 4: Rebuild initramfs
log "[4/5] Rebuilding initramfs..."
sudo mkinitcpio -P 2>&1 | tee -a "$LOCAL_LOG"
# ============================================================
# 4. VERIFICATION
# ============================================================
section "Post-Recovery Verification"
log "Checking module loading..."
if sudo modprobe nvidia 2>&1; then
log "${GREEN}nvidia module loaded successfully$RESET"
else
log "${YELLOW}modprobe nvidia failed - module will be loaded at next boot$RESET"
fi
# Detect 580xx version for manual DKMS fallback
DKMS_VERSION=$(pacman -Qi nvidia-580xx-dkms 2>/dev/null | grep Version | awk '{print $2}' | cut -d- -f1 || echo "580.159.04")
# Manual DKMS fallback uses DKMS_VERSION variable
log ""
log "${BOLD}Recovery complete!${RESET}"
log ""
log "Before rebooting, verify:"
log " 1. Module exists: ls /usr/lib/modules/$(uname -r)/extra/nvidia*"
log " 2. nvidia-smi works: sudo nvidia-smi"
log ""
log "Then reboot:"
log " sudo reboot"
log ""
log "Log saved to: $LOCAL_LOG"
log "============================================="