Compare commits

...
66 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
ja dbf366f8a5 Update Hyprland keybindings and monitor refresh rates
- SUPER+Q: killactive (was kitty)
- SUPER+T: exec kitty (new terminal binding)
- SUPER+C: removed (was killactive)
- All monitors set to 60Hz (DP-1 and HDMI-A-1 down from 144Hz)
2026-05-28 08:25:57 +02:00
ja 6f455ab0f2 chore: remove alesi-routine-morning systemd timer
- Deleted alesi-routine-morning.service and .timer from ~/.config/systemd/user/
- Timer failed daily at 07:45 (exit 128, git outside repo)
- daily-routine.sh script retained for manual use
- Updated CHANGELOG.md and workstation-health.md
2026-05-26 08:59:00 +02:00
ja b0af3dcf12 Workstation optimization: Chrome cleanup, package purge, PCIe audit, service tuning
- Chrome/Chromium cleanup (ADR 0006): removed 10GB chromium profile, broken PWA
  shortcuts, dead BROWSER_CDP_URL from .bashrc, restored Wayland chrome-flags.conf
- Package cleanup (ADR 0007): Tier 1-4 executed, 40 packages removed (KDE Plasma,
  ruby, love, xorg-server, kscreen/kscreenlocker), 45 cascade orphans purged
- Waybar USB module: auto-mount via udisks2, yazi integration, hover details
- Service tuning: disabled bluetooth.service, NetworkManager-wait-online.service
- hypridle fix: upgraded sdbus-cpp 2.3.0->2.3.1-1 (SEGV crash loop resolved)
- PCIe audit: GPU in wrong slot (PCIEX4 vs PCIEX16), 8x bandwidth loss documented
- D-Bus cleanup: removed stale org.kde.kwalletd6.service stub
- Ansible: all changes reflected in vars/main.yml and task files
2026-05-22 16:24:18 +02:00
ja 2487aa5749 feat(chrome): NVIDIA Wayland GPU acceleration for Chrome 148
Replace obsolete WebGPU flags with state-of-the-art NVIDIA VA-API
hardware decode config for Chrome 148 on Wayland/Hyprland.

- Disable seccomp sandbox to fix NVIDIA GBM "Permission denied" error
- Disable VulkanFromANGLE/DefaultANGLEVulkan (incompatible with Wayland)
- Force ANGLE->OpenGL backend to avoid Vulkan/Wayland conflicts
- Enable modern VA-API flags: VaapiOnNvidiaGPUs, AcceleratedVideoDecodeLinux*
- Add gnome-libsecret password store for gnome-keyring integration
- Install libva-utils and libva-nvidia-driver packages
- Update driver version to 580.159.03 in hardware inventory

WebGPU remains hardware accelerated via ANGLE+OpenGL. Chrome verified
working with full video decode acceleration (H.264, VP9, HEVC) via NVDEC.
2026-05-21 16:02:11 +02:00
ja 7d8760592e chore: align IaC with today's package installs + PDF comparison docs
DevOps sync (2026-05-21):
- Remove onlyoffice-desktopeditors from absent list (actively used)
- Add gnome-keyring, rbw to common role (secrets migration)
- Add perl-image-exiftool, icoutils, innoextract-git to dev-tools
  (Windows installer inspection)
- Create fonts role: Inter, Cascadia Code, Fira, Source Sans, TeX Gyre
- Create pdf role: zathura, zathura-pdf-mupdf, python-weasyprint
- Register new roles in workstation.yml

PDF comparison:
- Add pdf-comparison docs with Quarto/Typst/Pandoc outputs
- Add batch-pdf.sh script
- Update yazi-cheatsheet.md
- Ignore .quarto/ cache
2026-05-21 13:38:50 +02:00
ja d746d76530 migrate secrets management from KWallet to gnome-keyring + Bitwarden
Architecture: Bitwarden (central vault) → rbw CLI → gnome-keyring
(local cache via org.freedesktop.secrets D-Bus). All apps (himalaya,
git, ssh-agent, JetBrains IDEs, browsers) now read secrets via
libsecret/secret-tool through gnome-keyring.

Changes:
- Install gnome-keyring + rbw, enable as systemd user service
- Configure rbw → bitwarden.jantec.xyz (ironmikechw+bitwarden@gmail.com)
- Migrate 45 meaningful entries from KWallet → BW + GK
- Switch git credential.helper from store to libsecret
- Document: one-pager, ADR, migration checklist, NEXT_STEPS update
- Skill: devops/secrets-migration for reuse

Post-reboot manual: himalaya IMAP passwords (4 accounts, were not in
KWallet) and GitHub CLI token re-auth.

See docs/product/passwords.md for the full guide.
2026-05-21 12:58:38 +02:00
ja a9bc934543 chore: add LibreOffice lock files to .gitignore 2026-05-21 11:16:29 +02:00
ja ee592cc21b feat: add start-hyprland watchdog override for uwsm session
Hyprland 0.53+ requires launching via start-hyprland (watchdog) for
proper crash diagnostics and signal handling. Without it a warning
banner appears on login.

Override package-managed hyprland.desktop with local copy pointing to
start-hyprland. XDG_DATA_DIRS priority ensures the override wins
without modifying system files.

- ansible/roles/hyprland/tasks/main.yml: manage override in ~/.local/share/wayland-sessions/
- docs/adr/0004-start-hyprland-override.md: full decision record
- docs/hyprland-uwsm.md: document override behavior
2026-05-21 11:15:14 +02:00
ja f81c789683 feat(docs): add Hyprland keybindings and screenshot cheatsheet 2026-05-21 09:25:30 +02:00
ja 2bf5f5c005 ansible: sync templates with live system state
- hyprland: add pulseaudio + custom/gpu modules to waybar
- hyprland: template hyprlauncher.conf
- hyprland: add keybinding updates (yazi, chrome, screenshots, reload)
- hyprland: add uwsm package, replace wofi with hyprlauncher
- hyprland: add waybar GPU monitoring script
- greetd: update tuigreet command to uwsm start
- vars: add uwsm, hyprlauncher; remove wofi; add screenshot tools
2026-05-21 09:10:24 +02:00
ja 1fcb467c04 feat: uwsm integration and hyprland worktime saver keybinds
- Replace SDDM with greetd+tuigreet (VT1) using uwsm-managed Hyprland
- Remove legacy exec-once from hyprland.conf
- Create 6 systemd user services: hyprpaper, waybar, dunst, hypridle,
  hyprlauncher, cliphist (all WantedBy=graphical-session.target)
- Add worktime saver keybinds: fullscreen, float, cycle windows,
  scratchpad, reload config, browser, keybind menu
- Home row navigation: HJKL for focus, move, resize (SUPER+ALT)
- Replace wofi with hyprlauncher (first-party, uwsm app prefix)
- Add hyprlauncher config with finders: desktop, math, unicode, font
- New ansible role templates: systemd/user services
- Documentation: hyprland-uwsm.md, cheatsheet.txt

KDE Wallet -> Bitwarden migration deferred (extract script prepared)
2026-05-20 16:02:53 +02:00
ja 9d06a71f71 feat: migrate from SDDM to greetd+tuigreet
- Add greetd role with tuigreet greeter (3 packages, 4.9MB)
- Replace SDDM as display-manager.service with greetd
- Add ADR 0003 documenting migration rationale
- Rename hyprland task: "register session in SDDM" -> "register
  Wayland session desktop entry"
- Add display_manager variables to vars/main.yml
- Add greetd role to workstation.yml
- Update setup.md timestamp
2026-05-20 12:51:25 +02:00
ja 1274b3e2cb feat: add hyprpaper wallpaper management with Next manga wallpaper 2026-05-20 12:31:21 +02:00
ja 89acae9751 feat(ansible/hyprland): enable left-handed mouse (pixart-usb-optical-mouse) 2026-05-20 12:09:02 +02:00
ja 7a95ef0019 fix(ansible/hyprland): standardize keybindings (Super+Q open, Super+C close)
Change default Hyprland keybindings to match convention:
- Super+Q now opens kitty (was Return)
- Super+C now kills active window (was Q)
2026-05-20 12:02:34 +02:00
ja a67c629356 fix(ansible/hyprland): update template for Hyprland 0.55+ syntax
- Remove pseudotile (removed in 0.55)
- Change togglesplit to layoutmsg, togglesplit
2026-05-20 11:34:53 +02:00
jaandJunie cdb0730280 docs: update hardware/setup docs and add install script
Co-authored-by: Junie <junie@jetbrains.com>
2026-05-20 10:49:07 +02:00
jaandJunie bab92f0292 feat: add hyprland environment templates
Co-authored-by: Junie <junie@jetbrains.com>
2026-05-20 10:49:07 +02:00
jaandJunie 54d8729e63 feat: implement hyprland role and package configuration
Co-authored-by: Junie <junie@jetbrains.com>
2026-05-20 10:49:07 +02:00
jaandJunie e6f3506ef1 feat: add system-upgrade role for kernel/nvidia migration
Co-authored-by: Junie <junie@jetbrains.com>
2026-05-20 10:49:07 +02:00
jaandJunie 724ccebdb0 docs: add ADR 0002 for kernel and nvidia strategy
Co-authored-by: Junie <junie@jetbrains.com>
2026-05-20 10:49:07 +02:00
jaandJunie 45ad579120 feat: configure systemd power management and fix kitty TERM
Co-authored-by: Junie <junie@jetbrains.com>
2026-05-20 10:49:07 +02:00
jaandJunie 6ff790fd4b fix(ansible): optimize root trim policy
Co-authored-by: Junie <junie@jetbrains.com>
2026-04-02 16:15:32 +02:00
ja bbdcd43e01 docs(tech): add post-firmware benchmark and complete GXA7801Q update docs
- Post-update fio benchmark: random read +12.4%, write +15.9% IOPS
- Latency reduced 10-14% for random workloads
- SMART health unchanged: 0 media errors, 0 error log entries
- Firmware research status updated to complete
- Pre-benchmark checklist marked done
2026-04-02 15:10:54 +02:00
ja 8fec0802ad chore(eod): pre-reboot cleanup and documentation update
- Add scripts/update-cli-suite.sh for Manjaro/Arch CLI tool maintenance
- Update .gitignore: add logs/ and docs/tech/capabilities/ exclusions
- Update CHANGELOG.md with 2026-04-02 NVMe benchmark + firmware work
- Update NEXT_STEPS.md: post-reboot verification tasks for GXA7801Q
2026-04-02 14:34:00 +02:00
ja 5d770d2def docs(tech): correct firmware target GXA7801Q (non-SED) and update status
- Key discovery: GXA7802Q is SED-only (00B07), our drive needs GXA7801Q (00B00)
- Updated all references from GXA7802Q to GXA7801Q
- Added firmware naming convention table (1Q=non-SED, 2Q=SED)
- Updated procedure with correct GitHub source and slot-based update
- Marked firmware update as applied (reboot pending)
- Added SHA256 checksum for GXA7801Q.bin
2026-04-02 10:57:12 +02:00
ja 1a57375fff docs(tech): pre-firmware benchmark + GXA7801Q update applied
- Comprehensive NVMe benchmark before firmware update (GXA7401Q)
- SMART health: 19% used, 0 media errors, 100% spare
- Sequential read: 1,554 MiB/s, write: 1,305 MiB/s
- Random read: 361k IOPS, write: 291k IOPS
- Key discovery: GXA7802Q is SED-only, GXA7801Q is non-SED (our drive)
- Firmware update applied: fw-download + fw-commit + fw-activate (slot 2)
- Reboot required to activate GXA7801Q firmware
2026-04-02 10:53:54 +02:00
ja 66408ff68f docs(tech): add pre-firmware-update NVMe benchmark (GXA7401Q)
Comprehensive benchmark of Samsung PM9A1 NVMe SSD before firmware
update from GXA7401Q to GXA7802Q. Includes SMART health, hdparm,
and fio results (seq/rand read/write, 4K blocks, iodepth=32).
2026-04-02 10:40:10 +02:00
ja 7a672833aa docs(tech): add PM9A1 firmware changelog GXA7401Q to GXA7802Q
- Document full firmware progression: GXA7401Q → GXA7602Q → GXA7702Q → GXA7802Q
- GXA7802Q fixes unmap/TRIM handling regressions from GXA7702Q
- Add sources: Samsung PDF, Win-Raid, GitHub samsung-nvme-firmware
2026-04-02 10:15:50 +02:00
ja b80dad6a75 feat(tech): add fwupd setup and firmware update research
- Install fwupd 2.1.1, refresh LVFS metadata
- Detect 7 devices: NVMe, SATA SSD, CPU, AMD TPM 2.0, 3 displays
- LVFS: no updates published for Samsung SSDs
- New discovery: AMD fTPM 2.0 (v3.8.0.3) not in original inventory
- NVMe firmware GXA7802Q available via nvme-cli (not LVFS)
- Action plan: 1 medium-risk update (NVMe), rest up-to-date or EOL
2026-04-02 10:02:15 +02:00
ja 9494d1693b docs(tech): add firmware/BIOS update research for all workstation components
- Researched 7 component categories for available firmware updates
- NVMe SSD (PM9A1): update available GXA7401Q -> GXA7802Q
- Motherboard BIOS F53 is current (no update needed)
- SATA SSD 840 EVO, GPU GTX 1050 Ti: EOL, no updates
- NIC/CPU microcode: managed via kernel/firmware packages
- USB peripherals: no LVFS/fwupd support
- Recommend installing fwupd for future firmware management
2026-04-02 09:49:00 +02:00
ja 5a1ae7b0e2 docs(tech): add hardware inventory and document LLM model migration
- Created docs/tech/hardware-inventory.md with full system specs
- Documented LLM model move to /home/mw/models in manual-overrides.md
2026-04-02 09:38:02 +02:00
ja 6980d7369a docs(tech): add hardware inventory, fix DDR5->DDR4 error in health doc
- New docs/tech/hardware-inventory.md with full hardware specs:
  CPU (Ryzen 7 2700X), RAM (64GiB DDR4-2666, 4x16GiB), storage
  (512G NVMe PM9A1 + 500G SATA 840 EVO), GPU (GTX 1050 Ti),
  networking (Realtek 1Gbps + Tailscale), USB peripherals, chipset
- Fix workstation-health.md: DDR5-3200 was incorrect, actual DDR4-2666
- Note: primary disk at 93% usage (34G free)
2026-04-02 09:18:06 +02:00
ja 1ae898a9af feat(dev-tools): expand Ansible role with complete CLI tool inventory
Add all tools from infra.standards.md §4 and global Skills requirements:
- CLI utilities: zoxide, fzf, gum, dust, bottom, scrot, shellcheck, shfmt
- Git CLIs: github-cli, tea, glab
- Python: ruff, uv
- DevOps: terraform, docker suite, podman, cmake
- Security: cosign, syft, trivy, certbot, dive
- Docs: typst, pandoc, wp-cli
- CalDAV: khal, vdirsyncer
- 3D/CAD: openscad, freecad, f3d
- Node.js global: @marp-team/marp-cli
- Python pipx: semgrep, huggingface_hub[cli]
2026-03-31 13:00:47 +02:00
jaandJunie ccdaf95f15 docs(security): add AppArmor, systemd sandboxing, Falco, YARA, cosign, OIDC, Ansible integration
Complete hardening workflow now covers all key technical concepts:
- AppArmor/SELinux status, systemd sandboxing audit
- Semgrep SAST, YARA malware patterns, Nuclei network scan
- Cosign artifact verification, SHA256 package verification
- Falco/eBPF runtime monitoring, Volatility 3 memory forensics
- OIDC/GAT token best practices
- Ansible integration per .clinerules/workstation.md

Co-authored-by: Junie <junie@jetbrains.com>
2026-03-24 04:35:50 +01:00
jaandJunie e0c2997ee4 docs(security): add workstation hardening workflow for Arch Linux
Daily/weekly/monthly security checks covering systemd health,
network C2 detection, package audit, SBOM scanning, Unicode IOC
sweep, kernel hardening, and firewall status. References
supply-chain-security-2026-03.md for detailed threat context.

Co-authored-by: Junie <junie@jetbrains.com>
2026-03-24 04:34:31 +01:00
jaandJunie 8670ecd787 docs(security): expand supply chain defense guide with deep research findings
Co-authored-by: Junie <junie@jetbrains.com>
2026-03-23 23:16:34 +01:00
ja 519142f0d7 docs: update workstation health with crashlog analysis 2026-03-17 23:27:53 +01:00
ja 9a5d81124c docs: update workstation health documentation with system check results 2026-03-17 23:25:22 +01:00
ja 54ab23dce9 docs(system): analysis reports and EOD workflows
- Created crash analysis for 2026-03-17 OOM event.
- Documented stability test results and memory tuning effects.
- Added PARA-structured workflows for EOD protocol.
- Updated workstation health report for current 16GB swap status.
2026-03-17 19:02:06 +01:00
ja 40d073f0dc feat(scripts): implement eod argument in daily-routine.sh
- Integrated maintenance.sh into the end-of-day protocol.
- Added hygiene and documentation checklists to EOD workflow.
- Updated daily-routine.sh with morning and evening phases.
2026-03-17 19:02:03 +01:00
ja 72274e0d39 feat(ansible): increase swap to 16GB and apply memory optimizations
- Resized swap file from 4GB to 16GB.
- Configured zswap pool (30%) and THP (madvise) via GRUB.
- Tuned kernel parameters for proactive reclaim and NVMe write-back.
- Integrated systemd-oomd for user slice monitoring.
- Updated documentation and changelog.
2026-03-17 19:02:01 +01:00
jaandJunie d2067dfb91 feat: implement 7-phase morning start protocol and enhanced health checks
Co-authored-by: Junie <junie@jetbrains.com>
2026-03-17 16:50:55 +01:00
jaandJunie 4c489ded5c chore: ignore .junie memory directory
Co-authored-by: Junie <junie@jetbrains.com>
2026-03-17 16:46:55 +01:00
ja cd7d9a6746 fix(scripts): suppress checkupdates pipefail error in daily-routine.sh 2026-03-16 11:36:13 +01:00
ja 67d299d8ad feat(scripts): add morning health check and daily routine scripts 2026-03-16 10:54:30 +01:00
ja 69d6757d51 feat(scripts): add morning health check and daily routine scripts 2026-03-16 10:53:10 +01:00
102 changed files with 15604 additions and 75 deletions
+12
View File
@@ -15,8 +15,12 @@ CLAUDE.local.md
# Logs
*.log
logs/
.rlm/logs/
# Auto-generated capability discovery
docs/tech/capabilities/
# Dependencies
node_modules/
venv/
@@ -26,3 +30,11 @@ __pycache__/
# Infrastructure
.terraform/
*.tfstate*
# AI Agent memory
.junie/
# LibreOffice lock files
.~lock.*#
/.quarto/
+5
View File
@@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ComposerSettings">
<execution />
</component>
</project>
+86
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
@@ -25,8 +29,90 @@ This repository manages the infrastructure, configuration, and maintenance for M
- **NEVER** commit raw secrets to the repository.
- **ALWAYS** run Ansible with `--check` before applying changes.
- **NEVER** modify system configuration directly if it can be managed via Ansible.
- **Secrets Management**: All secrets stored in Bitwarden (`bitwarden.jantec.xyz`) and cached locally in `gnome-keyring` (D-Bus secrets service). See `docs/product/passwords.md` for details.
## Rules Hierarchy
- 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.
+58 -3
View File
@@ -7,13 +7,68 @@ 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).
- X11 package removal list `hyprland_packages_remove` in `ansible/vars/main.yml` with corresponding task in `hyprland` role — removes `xclip` and `xsel` on Wayland-only system (2026-05-22).
- ADR 0006: Chrome/Chromium normalization and cleanup decision record (2026-05-22).
- New Ansible role `ansible/roles/system-upgrade/` that runs as the **first** role in `workstation.yml`. Performs a full `pacman -Syu`, dual-installs `linux612` (LTS, fallback) and `linux70` (stable, new default) kernels with matching headers, and verifies both initramfs images and GRUB menu entries post-install. Invoke stand-alone with `ansible-playbook ansible/workstation.yml --tags system-upgrade`; idempotent on re-runs (2026-05-18).
- `docs/adr/0002-kernel-and-nvidia-strategy.md` — ADR documenting the LTS+stable kernel choice and the `575xx → 595.71` NVIDIA migration for Hyprland/Wayland preparation (2026-05-18).
- Pre-flight safety gate in the new role: aborts if free space on `/` drops below `min_free_root_gb` (5 GiB by default) (2026-05-18).
- Comprehensive NVMe benchmark for Samsung PM9A1 before firmware update: SMART health, hdparm, fio seq/rand benchmarks (2026-04-02). See `docs/tech/nvme-benchmark-pre-gxa7802q.md`.
- CLI suite update script `scripts/update-cli-suite.sh` for Manjaro/Arch tool maintenance (2026-04-02).
- Created formal stability tests report documenting the 2026-03-17 memory management improvements (2026-03-17).
- System stability improvements: enabled `systemd-oomd` and `zswap` via Ansible (2026-03-17).
- Memory resource limits for Firebird and Mailpit Docker containers (2026-03-17).
- Implemented and verified advanced memory optimizations: zswap 30% pool, THP madvise, and proactive kernel reclaim tuning (2026-03-17).
- Integrated 16GB swap file management into Ansible roles for consistency and persistence (2026-03-17).
- 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).
- Tier 3 package cleanup (ADR 0007): removed ruby (38 packages), love, xorg-server, xf86-input-libinput. No active Ruby usage; no .love files; xorg-server no longer required (xorg-xwayland is independent). Frees ~30 MiB (2026-05-22).
- Tier 2b: removed kscreen, kscreenlocker (KDE screen management) + 10 cascade orphan deps (libkscreen, kdeclarative, plasma5support, libksysguard, etc.). Freed 35 MiB. Replaced by wlr-randr/hyprlock (2026-05-22).
- Removed stale `org.kde.kwalletd6.service` D-Bus stub from `~/.local/share/dbus-1/services/` (was a broken `Exec=/bin/true` placeholder) (2026-05-22).
- Disabled `bluetooth.service` (no BT hardware) and `NetworkManager-wait-online.service` (unnecessary on wired workstation, speeds up boot) (2026-05-22).
- Documented PCIe slot misconfiguration: GPU (GTX 1050 Ti) installed in bottom PCIEX4 slot (PCIe 2.0 x4, ~2 GB/s) instead of top PCIEX16 (PCIe 3.0 x16, ~16 GB/s). 8x bandwidth loss. Physical move required. NVMe also running at Gen2 instead of Gen3 (2026-05-22).
### Changed
- Updated system status in `README.md` and `docs/tech/workstation-health.md` (2026-03-13).
- Tier 4 package reclassification (ADR 0007): 13 "Misc" packages reclassified to functional domains in `docs/tech/stack.md` — uv→Python Toolchain, jq→Shell/Terminal, inxi→Hardware, lsof/zram-generator/mkinitcpio-openswap→Core OS, iproute2/openresolv→Network, perl-image-exiftool/icoutils/innoextract-git→Dev Tools, opencl-headers→GPU Compute, libxnvctrl-580xx→NVIDIA. No packages removed; documentation-only change (2026-05-22).
- Chrome/Chromium normalization: removed all Chromium artifacts — `~/.config/chromium/` (1.4 GB), `~/.local/chrome-cdp/` (8.6 GB), 13 broken PWA `.desktop` files, 4 stale PWA manifests from Chrome Preferences, `chromium-widevine` orphan package (17 MB). System now uses `google-chrome-stable` exclusively with Wayland-native flags (2026-05-22).
- `~/.bashrc`: removed dead `BROWSER_CDP_URL` auto-population (curl to localhost:9222 on every shell startup) (2026-05-22).
- `~/.config/chrome-flags.conf`: restored Ansible-managed Wayland flags (`--ozone-platform=auto`, VA-API on NVIDIA, `--use-angle=gl`, Vulkan disable) — was drifted to X11 mode with garbage flags (2026-05-22).
- Waybar config: added `custom/usb` module to `modules-right` bar (2026-05-22).
- Waybar style: added CSS for `#custom-usb` with hover state and color classes (`usb-none`, `usb-unmounted`, `usb-mounted`) (2026-05-22).
- **Executed `system-upgrade` playbook on the live workstation** (2026-05-18). `pacman -Syu` upgraded **1103 packages**; kernel set now `linux612 6.12.85-1` (LTS, was 6.12.77-1) + `linux70 7.0.3-1` (NEW); NVIDIA migrated to mainline `595.71.05` for both `linux612-nvidia` and `linux70-nvidia`; legacy `*-nvidia-575xx` packages removed. Both `/boot/initramfs-6.12-x86_64.img` (54 MB) and `/boot/initramfs-7.0-x86_64.img` (122 MB) generated; GRUB regenerated with menuentries for both kernels. **Reboot required**: select `linux70` from the GRUB "Advanced options for Manjaro Linux" submenu to activate the new kernel and NVIDIA 595.71.05 modules. Logs: `logs/system-upgrade-20260518-101253-live.log`.
- NVIDIA driver migrated from the pinned `*-nvidia-575xx` (575.64.05) branch to the mainline `*-nvidia` (595.71.05) branch via the new `system-upgrade` role. `linux612-nvidia-575xx`, `nvidia-575xx-utils`, `lib32-nvidia-575xx-utils`, `nvidia-575xx-settings` are removed; `linux612-nvidia`, `linux70-nvidia`, `nvidia-utils`, `lib32-nvidia-utils`, `opencl-nvidia`, `nvidia-settings` are installed in one atomic pacman transaction (2026-05-18).
- `ansible/workstation.yml`: `system-upgrade` registered as the first role, guarded by `when: os_type == 'archlinux'`, with tag `system-upgrade` (2026-05-18).
- `ansible/vars/main.yml`: added `kernel_packages`, `nvidia_kernel_modules`, `nvidia_userspace_packages`, `nvidia_packages_to_remove`, `min_free_root_gb` so future kernel/driver pins are a one-line change (2026-05-18).
- `docs/tech/setup.md`: new bootstrap step "Run system upgrade & kernel install" before the main playbook, with `--tags system-upgrade` examples and a reboot-into-`linux70` callout (2026-05-18).
- Samsung PM9A1 NVMe firmware update applied: GXA7401Q → GXA7801Q (non-SED variant, reboot pending to activate). All nvme-cli steps succeeded (2026-04-02).
- Corrected firmware research: GXA7802Q is SED-only (`00B07`); non-SED drives (`00B00`) require GXA7801Q. Updated `docs/tech/firmware-update-research.md` with naming convention and correct source.
- Updated `.gitignore`: added `logs/` and `docs/tech/capabilities/` exclusions (2026-04-02).
- Increased system swap file size from 4GB to 16GB to prevent OOM-related system hangs (2026-03-17).
- Updated system status in `README.md` and `docs/tech/workstation-health.md` (2026-03-17).
- Cleaned up deprecated software information from `docs/tech/stack.md` (2026-03-13).
- Synchronized software versions across documentation files (2026-03-13).
### Fixed
- Identified failed `archlinux-keyring-wkd-sync.service` in system health check.
+11 -10
View File
@@ -8,7 +8,7 @@ tags:
- infrastructure-as-code
- linux
- automation
last_updated: '2026-03-13'
last_updated: '2026-06-17'
---
# saTway® Infrastructure - mw-pfeddersheim-workstation
@@ -28,27 +28,28 @@ ansible-playbook ansible/workstation.yml
# 3. Run automated maintenance
bash scripts/maintenance.sh
# 4. Run daily routine (Morning/EOD)
bash scripts/daily-routine.sh morning-full
bash scripts/daily-routine.sh eod
```
## 📊 Current Status (2026-03-13)
- **System Health**: ✅ Healthy (Load: 1.59, Disk: 84% used, Memory: 44% used)
- **Active Containers**: 6 running
- **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
@@ -0,0 +1,20 @@
project:
type: default
output-dir: "./docs/pdf-output"
format:
typst-pdf:
default: true
toc: false
number-sections: true
mainfont: "Inter"
monofont: "JetBrains Mono"
fontsize: 9pt
papersize: a4
margin:
top: 1.2cm
bottom: 1.2cm
left: 1.5cm
right: 1.5cm
lang: de
+25
View File
@@ -0,0 +1,25 @@
---
- name: Update GRUB
command: update-grub
become: yes
tags: [grub, boot]
- name: Restart systemd-oomd
systemd:
name: systemd-oomd
state: restarted
become: yes
tags: [systemd, oomd]
- name: Restart systemd-logind
systemd:
name: systemd-logind
state: restarted
become: yes
tags: [systemd]
- name: Reload systemd
systemd:
daemon_reload: yes
become: yes
tags: [systemd]
+238 -1
View File
@@ -1,4 +1,121 @@
---
- 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 }}"
register: swap_stat
tags: [swap, optimization]
- name: Check if swap is active
command: swapon --show
register: swapon_status
changed_when: false
tags: [swap, optimization]
- name: Turn off swap before resize
command: "swapoff {{ swap_file_path }}"
when: swap_stat.stat.exists and (swap_stat.stat.size != (swap_file_size_gb | int * 1024 * 1024 * 1024)) and (swap_file_path in swapon_status.stdout)
become: yes
tags: [swap, optimization]
- name: Create or resize swap file
command: "fallocate -l {{ swap_file_size_gb }}G {{ swap_file_path }}"
when: not swap_stat.stat.exists or (swap_stat.stat.size != (swap_file_size_gb | int * 1024 * 1024 * 1024))
become: yes
register: swap_resized
tags: [swap, optimization]
- name: Set permissions on swap file
file:
path: "{{ swap_file_path }}"
mode: '0600'
owner: root
group: root
become: yes
tags: [swap, optimization]
- name: Format swap file (if resized or new)
command: "mkswap {{ swap_file_path }}"
when: swap_resized.changed
become: yes
tags: [swap, optimization]
- name: Enable swap file
command: "swapon {{ swap_file_path }}"
when: swap_resized.changed
become: yes
tags: [swap, optimization]
- name: Ensure swap is in fstab
mount:
name: none
src: "{{ swap_file_path }}"
fstype: swap
opts: defaults
passno: 0
dump: 0
state: present
become: yes
tags: [swap, optimization]
- name: Check current root mount options
command:
argv:
- findmnt
- -no
- OPTIONS
- /
register: root_mount_options
changed_when: false
tags: [filesystem, optimization, trim]
- name: Ensure root filesystem uses periodic TRIM settings in fstab
mount:
path: /
src: "UUID={{ root_filesystem_uuid }}"
fstype: ext4
opts: "{{ root_filesystem_mount_opts }}"
passno: 1
dump: 0
state: present
become: yes
tags: [filesystem, optimization, trim]
- name: Reload systemd after fstab updates
systemd:
daemon_reload: yes
changed_when: false
become: yes
tags: [filesystem, optimization, trim]
- name: Remount root filesystem without continuous discard
mount:
path: /
opts: "{{ root_filesystem_mount_opts }},nodiscard"
state: remounted
when: "'discard' in (root_mount_options.stdout.split(','))"
become: yes
tags: [filesystem, optimization, trim]
- name: Optimize kernel parameters
sysctl:
name: "{{ item.name }}"
@@ -9,6 +126,9 @@
- { name: 'vm.swappiness', value: '10' }
- { name: 'fs.inotify.max_user_watches', value: '524288' }
- { name: 'vm.vfs_cache_pressure', value: '50' }
- { name: 'vm.dirty_ratio', value: '10' }
- { name: 'vm.dirty_background_ratio', value: '5' }
- { name: 'vm.watermark_scale_factor', value: '100' }
become: yes
tags: [optimization, sysctl]
@@ -18,4 +138,121 @@
regexp: '^#?KillUserProcesses='
line: 'KillUserProcesses=no'
become: yes
tags: [systemd, optimization]
tags: [systemd, optimization]
- name: Ensure systemd drop-in directories exist
file:
path: "{{ item }}"
state: directory
owner: root
group: root
mode: '0755'
with_items:
- /etc/systemd/logind.conf.d
- /etc/systemd/sleep.conf.d
become: yes
tags: [systemd, power]
- name: Configure systemd-logind to prevent sleep on keys/lid
copy:
dest: /etc/systemd/logind.conf.d/prevent-sleep.conf
content: |
[Login]
HandleSuspendKey=ignore
HandleHibernateKey=ignore
HandleLidSwitch=ignore
owner: root
group: root
mode: '0644'
become: yes
notify: Restart systemd-logind
tags: [systemd, power]
- name: Configure systemd-sleep to disable sleep/hibernation
copy:
dest: /etc/systemd/sleep.conf.d/prevent-sleep.conf
content: |
[Sleep]
AllowSuspend=no
AllowHibernation=no
AllowSuspendThenHibernate=no
AllowHybridSleep=no
owner: root
group: root
mode: '0644'
become: yes
tags: [systemd, power]
- name: Ensure systemd directories exist
file:
path: "{{ item }}"
state: directory
with_items:
- /etc/systemd/system/user.slice.d
become: yes
tags: [oomd, systemd, stability]
- name: Configure ManagedOOM for user.slice
template:
src: 10-oomd-user.slice.conf.j2
dest: /etc/systemd/system/user.slice.d/10-oomd-user.slice.conf
become: yes
notify: Reload systemd
tags: [oomd, systemd, stability]
- name: Enable systemd-oomd
systemd:
name: systemd-oomd
enabled: yes
state: started
become: yes
tags: [systemd, oomd, stability]
- name: Configure kernel features in GRUB (Safe script)
shell: |
# Extract current cmdline
CURRENT_CMDLINE=$(grep '^GRUB_CMDLINE_LINUX_DEFAULT=' /etc/default/grub | cut -d'"' -f2)
# Remove our target parameters to avoid duplicates
NEW_CMDLINE=$(echo "$CURRENT_CMDLINE" | sed -E 's/\bzswap\.(enabled|max_pool_percent)=[0-9]+\b//g' | sed -E 's/\btransparent_hugepage=[a-z]+\b//g' | xargs)
# Append the desired parameters
FINAL_CMDLINE="$NEW_CMDLINE zswap.enabled=1 zswap.max_pool_percent=30 transparent_hugepage=madvise"
# Update the file if changed
if [ "$CURRENT_CMDLINE" != "$FINAL_CMDLINE" ]; then
sed -i "s|^GRUB_CMDLINE_LINUX_DEFAULT=.*|GRUB_CMDLINE_LINUX_DEFAULT=\"$FINAL_CMDLINE\"|" /etc/default/grub
echo "changed"
fi
become: yes
register: grub_cmdline_safe
changed_when: grub_cmdline_safe.stdout == "changed"
tags: [grub, boot, optimization, stability]
- name: Apply GRUB changes (Update GRUB)
command: update-grub
when: grub_cmdline_safe.changed
become: yes
tags: [grub, boot, optimization, stability]
- name: Set runtime Transparent Hugepages to madvise
shell: echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
become: yes
changed_when: false
tags: [optimization, thp]
- name: Set runtime zswap max_pool_percent
shell: echo 30 > /sys/module/zswap/parameters/max_pool_percent
become: yes
changed_when: false
tags: [optimization, zswap]
- name: Ensure secrets management tools are installed
community.general.pacman:
name:
# Secrets bridge (KWallet migration)
- gnome-keyring
# Bitwarden CLI client
- rbw
state: present
tags: [secrets, bitwarden]
@@ -0,0 +1,4 @@
[Slice]
ManagedOOMMemoryPressure=kill
ManagedOOMMemoryPressureLimit=60%
ManagedOOMSwap=kill
@@ -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
+144 -7
View File
@@ -2,16 +2,98 @@
- name: Ensure core workstation development tools are installed
community.general.pacman:
name:
# Build essentials
- git
- base-devel
# CLI tools (infra.standards.md §4)
- ripgrep
- ripgrep-all
- fd
- jq
- bat
- eza
- zoxide
- fzf
- gum
- dust
- bottom
- scrot
- shellcheck
- shfmt
# Git providers CLI
- github-cli
- tea
- glab
# Python toolchain
- ruff
- uv
# DevOps & IaC
- ansible
- terraform
- docker
- docker-buildx
- docker-compose
- podman
- cmake
# Security & supply chain
- cosign
- syft
- trivy
- certbot
# Container inspection
- dive
# Email CLI
- himalaya
# Document toolchain (markdown-to-pdf skill)
- typst
- pandoc
# WordPress CLI
- wp-cli
# C/C++ toolchain (cpp skill)
- clang
# Windows installer inspection (amicron-version-inspection skill)
- perl-image-exiftool
- icoutils
- innoextract-git
# CalDAV tools (caldav-webcal skill)
- khal
- vdirsyncer
# 3D/CAD tools (openscad, freecad, f3d skills)
- openscad
- freecad
- f3d
# Terminal
- kitty
# NVIDIA VA-API video decode for Chrome on Wayland
- libva-utils
- libva-nvidia-driver
state: present
- name: Ensure Node.js global CLI tools are installed
become: true
become_user: "{{ system_user }}"
command: pnpm add -g @marp-team/marp-cli
changed_when: false
tags: [nodejs, marp]
- name: Ensure Python CLI tools are installed via pipx
become: true
become_user: "{{ system_user }}"
command: "pipx install {{ item }}"
loop:
- semgrep
- huggingface_hub[cli]
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:
@@ -26,7 +108,6 @@
- bleachbit
- reiserfsprogs
- dmraid
- onlyoffice-desktopeditors
- virt-manager
- guvcview
- helvum
@@ -36,6 +117,7 @@
- oha
- unetbootin
- ventoy
- chromium-widevine
state: absent
- name: Ensure legacy editor packages are removed
@@ -46,15 +128,70 @@
- code-marketplace
state: absent
- name: Configure Chrome flags for WebGPU
- name: Configure Chrome flags for NVIDIA Wayland acceleration
copy:
dest: "/home/{{ system_user }}/.config/chrome-flags.conf"
content: |
--enable-unsafe-webgpu
--enable-vulkan
--enable-features=Vulkan
--ignore-gpu-blocklist
--ozone-platform=wayland
--disable-seccomp-filter-sandbox
--disable-features=Vulkan,VulkanFromANGLE,DefaultANGLEVulkan
--use-angle=gl
--enable-features=VaapiOnNvidiaGPUs,VaapiIgnoreDriverChecks,AcceleratedVideoDecodeLinuxZeroCopyGL,AcceleratedVideoDecodeLinuxGL
--password-store=gnome-libsecret
owner: "{{ system_user }}"
group: "{{ system_user }}"
mode: '0644'
tags: [chrome, webgpu]
tags: [chrome, nvidia, wayland]
- name: Remove dead BROWSER_CDP_URL from .bashrc
ansible.builtin.lineinfile:
path: "/home/{{ system_user }}/.bashrc"
regexp: '.*BROWSER_CDP_URL.*'
state: absent
become: yes
become_user: "{{ system_user }}"
tags: [chrome, shell, cleanup]
- name: Fix kitty cursor keys by removing TERM override in .bashrc
ansible.builtin.lineinfile:
path: "/home/{{ system_user }}/.bashrc"
regexp: '^export TERM=xterm-256color'
line: '# export TERM=xterm-256color # Fix: allow kitty to set its own TERM for cursor keys'
state: present
become: yes
become_user: "{{ system_user }}"
tags: [kitty, shell]
- name: Ensure kitty configuration directory exists
ansible.builtin.file:
path: "/home/{{ system_user }}/.config/kitty"
state: directory
owner: "{{ system_user }}"
group: "{{ system_user }}"
mode: '0755'
become: yes
become_user: "{{ system_user }}"
tags: [kitty]
- name: Configure kitty for proper terminal behavior
ansible.builtin.copy:
dest: "/home/{{ system_user }}/.config/kitty/kitty.conf"
content: |
# 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)
include /home/mw/.config/kitty/kitty-cline-keybindings.conf
owner: "{{ system_user }}"
group: "{{ system_user }}"
mode: '0644'
become: yes
become_user: "{{ system_user }}"
tags: [kitty]
+17
View File
@@ -0,0 +1,17 @@
---
- name: Ensure workstation fonts are installed
community.general.pacman:
name:
# Primary UI font (Inter brand font)
- inter-font
# Code editor fonts
- otf-cascadia-code
- ttf-fira-code
- otf-fira-mono
# Sans-serif families
- otf-fira-sans
- adobe-source-sans-fonts
# Serif fallback
- tex-gyre-fonts
state: present
tags: [fonts, typography]
+4
View File
@@ -0,0 +1,4 @@
---
- name: restart greetd
command: systemctl restart greetd
become: yes
+28
View File
@@ -0,0 +1,28 @@
---
- name: Greetd - install packages
pacman:
name:
- "{{ display_manager_package }}"
- "{{ display_manager_greeter }}"
state: present
update_cache: yes
tags: [greetd, packages]
- name: Greetd - configure tuigreet to launch Hyprland
template:
src: config.toml.j2
dest: "{{ display_manager_config }}"
owner: root
group: root
mode: '0644'
notify: restart greetd
tags: [greetd, config]
- name: Greetd - ensure enabled as display manager
file:
src: /usr/lib/systemd/system/greetd.service
dest: /etc/systemd/system/display-manager.service
state: link
force: yes
notify: restart greetd
tags: [greetd, service]
@@ -0,0 +1,8 @@
[terminal]
# The VT to run the greeter on
vt = 1
# Default session: tuigreet greeter with uwsm-managed Hyprland as fallback
[default_session]
command = "tuigreet --time --remember --asterisks --cmd 'uwsm start -e -D Hyprland hyprland.desktop'"
user = "greeter"
+4
View File
@@ -0,0 +1,4 @@
---
- name: rebuild initramfs
command: mkinitcpio -P
become: yes
+117
View File
@@ -0,0 +1,117 @@
---
- name: Hyprland - install packages
community.general.pacman:
name: "{{ hyprland_packages }}"
state: present
update_cache: yes
tags: [hyprland, packages]
- name: Hyprland - remove X11 packages (Wayland-only)
community.general.pacman:
name: "{{ hyprland_packages_remove }}"
state: absent
tags: [hyprland, packages]
- name: Hyprland - ensure configuration directories exist
file:
path: "/home/{{ system_user }}/.config/{{ item }}"
state: directory
owner: "{{ system_user }}"
group: "{{ system_user }}"
mode: '0755'
loop:
- hypr
- waybar
- hyprlauncher
- dunst
tags: [hyprland, config]
- name: Hyprland - ensure Waybar scripts directory exists
file:
path: "/home/{{ system_user }}/.config/waybar/scripts"
state: directory
owner: "{{ system_user }}"
group: "{{ system_user }}"
mode: '0755'
tags: [hyprland, config]
- name: Hyprland - configure NVIDIA DRM KMS (modeset=1)
lineinfile:
path: /etc/modprobe.d/nvidia.conf
line: "options nvidia-drm modeset=1"
create: yes
notify: rebuild initramfs
tags: [hyprland, nvidia]
- name: Hyprland - check if NVIDIA modules are already in mkinitcpio
command: grep -q "nvidia nvidia_modeset nvidia_uvm nvidia_drm" /etc/mkinitcpio.conf
register: mkinitcpio_check
failed_when: false
changed_when: false
tags: [hyprland, nvidia]
- name: Hyprland - configure early module loading in mkinitcpio
replace:
path: /etc/mkinitcpio.conf
regexp: '^MODULES=\((.*)\)'
replace: 'MODULES=(nvidia nvidia_modeset nvidia_uvm nvidia_drm \1)'
when: mkinitcpio_check.rc != 0
notify: rebuild initramfs
tags: [hyprland, nvidia]
- name: Hyprland - override Wayland session desktop entry (local user)
# Overrides /usr/share/wayland-sessions/hyprland.desktop with start-hyprland
# as the Exec target. This ensures Hyprland is launched through the watchdog
# binary (start-hyprland) for proper crash diagnostics and signal handling.
# XDG_DATA_DIRS priority: ~/.local/share > /usr/share.
# See: docs/adr/0004-start-hyprland-override.md
copy:
dest: /home/{{ system_user }}/.local/share/wayland-sessions/hyprland.desktop
owner: "{{ system_user }}"
group: "{{ system_user }}"
mode: '0644'
content: |
[Desktop Entry]
Name=Hyprland
Comment=An intelligent dynamic tiling Wayland compositor
Exec=start-hyprland
TryExec=start-hyprland
Type=Application
DesktopNames=Hyprland
tags: [hyprland, session]
- name: Hyprland - template core configuration
template:
src: "{{ item.src }}"
dest: "/home/{{ system_user }}/.config/{{ item.dest }}"
owner: "{{ system_user }}"
group: "{{ system_user }}"
mode: '0644'
loop:
- { 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' }
- { src: 'waybar_config.j2', dest: 'waybar/config' }
- { src: 'waybar_style.css.j2', dest: 'waybar/style.css' }
- { src: 'hyprlauncher.conf.j2', dest: 'hypr/hyprlauncher.conf' }
- { src: 'dunstrc.j2', dest: 'dunst/dunstrc' }
tags: [hyprland, config]
- name: Hyprland - deploy Waybar GPU monitoring script
template:
src: 'waybar_gpu_script.sh.j2'
dest: "/home/{{ system_user }}/.config/waybar/scripts/gpu.sh"
owner: "{{ system_user }}"
group: "{{ system_user }}"
mode: '0755'
tags: [hyprland, config]
- name: Hyprland - deploy Waybar USB automount script
template:
src: 'waybar_usb_script.sh.j2'
dest: "/home/{{ system_user }}/.config/waybar/scripts/usb.sh"
owner: "{{ system_user }}"
group: "{{ system_user }}"
mode: '0755'
tags: [hyprland, config]
@@ -0,0 +1,65 @@
[global]
monitor = 0
follow = mouse
width = 300
height = 300
origin = top-right
offset = 10x10
scale = 0
notification_limit = 0
progress_bar = true
progress_bar_height = 10
progress_bar_frame_width = 1
progress_bar_min_width = 150
progress_bar_max_width = 300
indicate_hidden = yes
transparency = 0
separator_height = 2
padding = 8
horizontal_padding = 8
text_icon_padding = 0
frame_width = 3
frame_color = "#aaaaaa"
separator_color = frame
sort = yes
idle_threshold = 120
font = Monospace 12
line_height = 0
markup = full
format = "<b>%s</b>\n%b"
alignment = left
vertical_alignment = center
show_age_threshold = 60
ellipsize = middle
ignore_newline = no
stack_duplicates = true
hide_duplicate_count = false
show_indicators = yes
icon_position = left
min_icon_size = 0
max_icon_size = 32
sticky_history = yes
history_length = 20
dmenu = /usr/bin/wofi -dmenu -p dunst:
browser = /usr/bin/xdg-open
always_run_script = true
title = Dunst
class = Dunst
corner_radius = 10
ignore_dbusclose = false
[urgency_low]
background = "#222222"
foreground = "#888888"
timeout = 10
[urgency_normal]
background = "#285577"
foreground = "#ffffff"
timeout = 20
[urgency_critical]
background = "#900000"
foreground = "#ffffff"
frame_color = "#ff0000"
timeout = 0
@@ -0,0 +1,16 @@
general {
lock_cmd = pidof hyprlock || hyprlock
before_sleep_cmd = loginctl lock-session
after_sleep_cmd = hyprctl dispatch dpms on
}
listener {
timeout = 300
on-timeout = loginctl lock-session
}
listener {
timeout = 330
on-timeout = hyprctl dispatch dpms off
on-resume = hyprctl dispatch dpms on
}
@@ -0,0 +1,154 @@
# hyprland.conf - managed by Ansible
# Monitor layout (left to right):
# 1. DP-1 TERRA 3280W 2560x1440@144 (0x0)
# 2. DVI-D-1 BenQ BL2405 1920x1080@60 (rotated 90° CW, effective width 1080)
# 3. HDMI-A-1 TERRA 3280W 2560x1440@144 (3640x0)
monitor=DP-1,2560x1440@60,0x0,1
monitor=DVI-D-1,1920x1080@60,2560x0,1,transform,1
monitor=HDMI-A-1,2560x1440@60,3640x0,1
# Session services: hyprpaper, waybar, dunst, hypridle, hyprlauncher, cliphist
# Managed by systemd user units under ~/.config/systemd/user/
# All WantedBy=graphical-session.target for uwsm integration
# Environment variables for NVIDIA
env = LIBVA_DRIVER_NAME,nvidia
env = XDG_SESSION_TYPE,wayland
env = GBM_BACKEND,nvidia-drm
env = __GLX_VENDOR_LIBRARY_NAME,nvidia
env = WLR_NO_HARDWARE_CURSORS,1
env = NVD_BACKEND,direct
# Input
input {
kb_layout = de
follow_mouse = 1
sensitivity = 0
numlock_by_default = true
}
device {
name = pixart-usb-optical-mouse
left_handed = 1
}
general {
gaps_in = 5
gaps_out = 10
border_size = 2
col.active_border = rgba(33ccffee) rgba(00ff99ee) 45deg
col.inactive_border = rgba(595959aa)
layout = dwindle
}
# Decoration
decoration {
rounding = 10
blur {
enabled = true
size = 3
passes = 1
}
}
# Animations
animations {
enabled = yes
}
# Dwindle layout
dwindle {
preserve_split = yes
}
# Keybindings
$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,
bind = $mainMod, D, exec, hyprlauncher
bind = $mainMod, P, pseudo
bind = $mainMod, J, layoutmsg, togglesplit
# Worktime safers
bind = $mainMod, F, fullscreen, 0 # Toggle fullscreen
bind = $mainMod, R, exec, hyprctl reload # Reload Hyprland config
bind = $mainMod, SPACE, togglefloating # Toggle float (popup window)
bind = $mainMod, TAB, cyclenext # Cycle through windows
bind = $mainMod SHIFT, TAB, cyclenext, prev # Cycle backwards
bind = $mainMod, GRAVE, togglespecialworkspace # Scratchpad (toggle)
bind = $mainMod SHIFT, GRAVE, movetoworkspace, special # Send to scratchpad
bind = $mainMod CTRL, K, exec, grep -E '^bind\s*=.*#' ~/.config/hypr/hyprland.conf | sed 's/^bind\s*=\s*\$mainMod/SUPER/;s/\s*#/#/' | fuzzel -d
# Home row navigation aliases (keep arrows too)
bind = $mainMod, H, movefocus, l
bind = $mainMod, L, movefocus, r
# HJKL + SHIFT = move window in direction (home row)
bind = $mainMod SHIFT, H, movewindow, l
bind = $mainMod SHIFT, L, movewindow, r
bind = $mainMod SHIFT, K, movewindow, u
bind = $mainMod SHIFT, J, movewindow, d
# Window resize via home row (SUPER+ALT)
bind = $mainMod ALT, H, resizeactive, -40 0
bind = $mainMod ALT, L, resizeactive, 40 0
bind = $mainMod ALT, K, resizeactive, 0 -40
bind = $mainMod ALT, J, resizeactive, 0 40
# Lock screen
bind = $mainMod CTRL, L, exec, hyprlock
# Screenshots
bind = , Print, exec, grimblast copysave area
bind = $mainMod, Print, exec, grimblast copysave active
bind = $mainMod SHIFT, Print, exec, grimblast copysave output
bind = $mainMod ALT, Print, exec, grim -g "$(slurp)" - | satty -f -
# Focus
bind = $mainMod, left, movefocus, l
# Workspace to monitor pinning
workspace = 1, monitor:DP-1, default:true
workspace = 2, monitor:DVI-D-1, default:true
workspace = 3, monitor:HDMI-A-1, default:true
bind = $mainMod, right, movefocus, r
bind = $mainMod, up, movefocus, u
bind = $mainMod, down, movefocus, d
# Move window to adjacent tile/monitor
bind = $mainMod SHIFT, left, movewindow, l
bind = $mainMod SHIFT, right, movewindow, r
bind = $mainMod SHIFT, up, movewindow, u
bind = $mainMod SHIFT, down, movewindow, d
# Apps
bind = $mainMod, B, exec, google-chrome-stable
# Workspaces
bind = $mainMod, 1, workspace, 1
bind = $mainMod, 2, workspace, 2
bind = $mainMod, 3, workspace, 3
bind = $mainMod, 4, workspace, 4
bind = $mainMod, 5, workspace, 5
bind = $mainMod, 6, workspace, 6
bind = $mainMod, 7, workspace, 7
bind = $mainMod, 8, workspace, 8
bind = $mainMod, 9, workspace, 9
# Move to workspace
bind = $mainMod SHIFT, 1, movetoworkspace, 1
bind = $mainMod SHIFT, 2, movetoworkspace, 2
bind = $mainMod SHIFT, 3, movetoworkspace, 3
bind = $mainMod SHIFT, 4, movetoworkspace, 4
bind = $mainMod SHIFT, 5, movetoworkspace, 5
bind = $mainMod SHIFT, 6, movetoworkspace, 6
bind = $mainMod SHIFT, 7, movetoworkspace, 7
bind = $mainMod SHIFT, 8, movetoworkspace, 8
bind = $mainMod SHIFT, 9, movetoworkspace, 9
@@ -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,
})
@@ -0,0 +1,16 @@
[general]
grab_focus = true
[cache]
enabled = true
[finders]
default_finder = desktop
unicode_prefix = .
math_prefix = =
font_prefix = '
desktop_launch_prefix = uwsm app --
desktop_icons = true
[ui]
window_size = 500 300
@@ -0,0 +1,29 @@
general {
no_fade_in = false
grace = 0
disable_loading = true
}
background {
monitor =
color = rgba(25, 20, 20, 1.0)
blur_passes = 2
}
input-field {
monitor =
size = 200, 50
outline_thickness = 3
dots_size = 0.33
dots_spacing = 0.15
dots_center = true
outer_color = rgb(151515)
inner_color = rgb(200, 200, 200)
font_color = rgb(10, 10, 10)
fade_on_empty = true
placeholder_text = <i>Input Password...</i>
hide_input = false
position = 0, -20
halign = center
valign = center
}
@@ -0,0 +1,4 @@
preload=/usr/share/wallpapers/Next/contents/images/5120x2880.png
wallpaper=DP-1,/usr/share/wallpapers/Next/contents/images/5120x2880.png
wallpaper=HDMI-A-1,/usr/share/wallpapers/Next/contents/images/5120x2880.png
wallpaper=DVI-D-1,/usr/share/wallpapers/Next/contents/images/5120x2880.png
@@ -0,0 +1,13 @@
[Unit]
Description=Cliphist clipboard store
PartOf=graphical-session.target
After=graphical-session.target
[Service]
Type=simple
ExecStart=/bin/sh -c '/usr/bin/wl-paste --type text --watch /usr/bin/cliphist store & /usr/bin/wl-paste --type image --watch /usr/bin/cliphist store & wait'
Restart=on-failure
RestartSec=3
[Install]
WantedBy=graphical-session.target
@@ -0,0 +1,13 @@
[Unit]
Description=Dunst notification daemon
PartOf=graphical-session.target
After=graphical-session.target
[Service]
Type=simple
ExecStart=/usr/bin/dunst
Restart=on-failure
RestartSec=3
[Install]
WantedBy=graphical-session.target
@@ -0,0 +1,13 @@
[Unit]
Description=Hyprland idle daemon
PartOf=graphical-session.target
After=graphical-session.target
[Service]
Type=simple
ExecStart=/usr/bin/hypridle
Restart=on-failure
RestartSec=3
[Install]
WantedBy=graphical-session.target
@@ -0,0 +1,14 @@
[Unit]
Description=Hyprlauncher daemon
PartOf=graphical-session.target
After=graphical-session.target
[Service]
Type=simple
ExecStartPre=/usr/bin/sleep 2
ExecStart=/usr/bin/hyprlauncher -d
Restart=on-failure
RestartSec=3
[Install]
WantedBy=graphical-session.target
@@ -0,0 +1,13 @@
[Unit]
Description=Hyprland Wallpaper daemon
PartOf=graphical-session.target
After=graphical-session.target
[Service]
Type=simple
ExecStart=/usr/bin/hyprpaper
Restart=on-failure
RestartSec=3
[Install]
WantedBy=graphical-session.target
@@ -0,0 +1,13 @@
[Unit]
Description=Waybar status bar
PartOf=graphical-session.target
After=graphical-session.target
[Service]
Type=simple
ExecStart=/usr/bin/waybar
Restart=on-failure
RestartSec=3
[Install]
WantedBy=graphical-session.target
@@ -0,0 +1,54 @@
{
"layer": "top",
"position": "top",
"height": 30,
"modules-left": ["hyprland/workspaces", "hyprland/mode"],
"modules-center": ["hyprland/window"],
"modules-right": ["network", "cpu", "memory", "custom/gpu", "pulseaudio", "custom/usb", "clock", "tray"],
"hyprland/workspaces": {
"disable-scroll": true,
"all-outputs": true,
"format": "{name}"
},
"clock": {
"format": "{:%Y-%m-%d %H:%M}",
"tooltip-format": "<big>{:%Y %B}</big>\n<tt><small>{calendar}</small></tt>"
},
"cpu": {
"format": "CPU {usage}%",
"tooltip": false
},
"memory": {
"format": "RAM {}%"
},
"network": {
"format-wifi": " {essid} ({signalStrength}%)",
"format-ethernet": "󰈀 {ifname}",
"format-disconnected": "⚠ Disconnected"
},
"pulseaudio": {
"format": "{icon} {volume}%",
"format-muted": "󰝟 Muted",
"format-icons": {
"default": ["", "", ""]
},
"on-click": "pavucontrol",
"on-click-right": "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle",
"on-scroll-up": "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+",
"on-scroll-down": "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"
},
"custom/gpu": {
"exec": "~/.config/waybar/scripts/gpu.sh",
"interval": 5,
"return-type": "json"
},
"custom/usb": {
"exec": "~/.config/waybar/scripts/usb.sh",
"interval": 10,
"return-type": "json",
"on-click": "~/.config/waybar/scripts/usb.sh open",
"on-click-right": "~/.config/waybar/scripts/usb.sh eject",
"format": "{}",
"tooltip": true
}
}
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Waybar GPU usage module - NVIDIA
set -euo pipefail
GPU=$(nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits 2>/dev/null | head -1 | tr -d ' ')
TEMP=$(nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader,nounits 2>/dev/null | head -1 | tr -d ' ')
if [ -z "$GPU" ]; then
echo '{"text": "GPU N/A", "percentage": 0}'
exit 0
fi
if [ "$GPU" -ge 80 ]; then
ICON=""
elif [ "$GPU" -ge 50 ]; then
ICON=""
else
ICON="󰢮"
fi
echo "{\"text\": \"${ICON} ${GPU}%\", \"tooltip\": \"GPU Usage: ${GPU}% | Temp: ${TEMP}°C\", \"percentage\": ${GPU}}"
@@ -0,0 +1,47 @@
* {
border: none;
border-radius: 0;
font-family: monospace;
font-size: 14px;
}
window#waybar {
background: rgba(43, 48, 59, 0.5);
color: #ffffff;
}
#workspaces button {
padding: 0 5px;
background: transparent;
color: #ffffff;
}
#workspaces button.active {
background: #64727D;
border-bottom: 3px solid #ffffff;
}
#clock, #cpu, #memory, #network, #tray, #pulseaudio, #custom-gpu, #custom-usb {
padding: 0 10px;
}
#pulseaudio:hover {
background: #555;
}
#custom-usb:hover {
background: #555;
}
#custom-usb.usb-none {
color: transparent;
padding: 0;
}
#custom-usb.usb-mounted {
color: #a3be8c;
}
#custom-usb.usb-unmounted {
color: #ebcb8b;
}
@@ -0,0 +1,166 @@
#!/usr/bin/env bash
# Waybar USB module - automount, status, open in yazi, eject
# Usage:
# usb.sh -> JSON status for waybar poll (auto-mounts unmounted USBs)
# usb.sh open -> open first mounted USB in yazi (via kitty)
# usb.sh eject -> unmount + power-off all USB devices
set -euo pipefail
# --- helpers -------------------------------------------------------------------
get_usb_devices() {
# Returns JSON array of USB block devices via lsblk.
# Fields: NAME, RM (removable), SIZE, LABEL, MOUNTPOINT, FSTYPE, TRAN, MODEL
lsblk -Jbpno NAME,RM,SIZE,LABEL,MOUNTPOINT,FSTYPE,TRAN,MODEL 2>/dev/null \
| jq -c '[.blockdevices[] | select(.tran == "usb")]'
}
auto_mount_unmounted() {
local devices="$1"
# Auto-mount any USB partition that isn't mounted yet
local unmounted
unmounted=$(echo "$devices" | jq -r '.[] | select(.mountpoint == null or .mountpoint == "") | .name')
for dev in $unmounted; do
udisksctl mount -b "$dev" --no-user-interaction 2>/dev/null || true
done
}
# --- actions -------------------------------------------------------------------
do_open() {
# Open the first mounted USB in yazi via kitty
local devices
devices=$(get_usb_devices)
local mountpoint
mountpoint=$(echo "$devices" | jq -r '[.[] | select(.mountpoint != null and .mountpoint != "")] | first | .mountpoint // empty')
if [ -z "$mountpoint" ]; then
# Try children (partitions)
mountpoint=$(lsblk -Jbpno NAME,RM,SIZE,LABEL,MOUNTPOINT,FSTYPE,TRAN,MODEL 2>/dev/null \
| jq -r '[.blockdevices[] | select(.tran == "usb") | .children[]? | select(.mountpoint != null and .mountpoint != "")] | first | .mountpoint // empty')
fi
if [ -n "$mountpoint" ]; then
kitty -e yazi "$mountpoint" &
disown
else
notify-send "USB" "No mounted USB device found" -i dialog-warning
fi
}
do_eject() {
# Unmount all USB partitions, then power-off the parent device
local devices
devices=$(get_usb_devices)
local mounted
mounted=$(echo "$devices" | jq -r '.[] | select(.mountpoint != null and .mountpoint != "") | .name')
# Also check children (partitions of whole disks)
local child_mounted
child_mounted=$(lsblk -Jbpno NAME,MOUNTPOINT,TRAN 2>/dev/null \
| jq -r '[.blockdevices[] | select(.tran == "usb") | .children[]? | select(.mountpoint != null and .mountpoint != "")] | .[].name')
for dev in $mounted $child_mounted; do
udisksctl unmount -b "$dev" --no-user-interaction 2>/dev/null || true
done
# Power-off the whole USB devices
local parent_devs
parent_devs=$(echo "$devices" | jq -r '.[].name')
for dev in $parent_devs; do
udisksctl power-off -b "$dev" --no-user-interaction 2>/dev/null || true
done
notify-send "USB" "All USB devices ejected" -i media-eject
}
do_status() {
local devices
devices=$(get_usb_devices)
# Auto-mount unmounted partitions
auto_mount_unmounted "$devices"
# Re-read after automount
devices=$(get_usb_devices)
# Count mounted USBs (check both top-level and children)
local count=0
local tooltip_lines=""
# Top-level devices
while IFS= read -r line; do
if [ -z "$line" ] || [ "$line" = "null" ]; then continue; fi
local name size label mount fstype model
name=$(echo "$line" | jq -r '.name')
size=$(echo "$line" | jq -r '.size')
label=$(echo "$line" | jq -r '.label // "N/A"')
mount=$(echo "$line" | jq -r '.mountpoint // "not mounted"')
fstype=$(echo "$line" | jq -r '.fstype // "?"')
model=$(echo "$line" | jq -r '.model // "USB" | gsub("^\\s+|\\s+$"; "")')
if [ "$mount" != "not mounted" ]; then
count=$((count + 1))
fi
tooltip_lines="${tooltip_lines}${name} ${model} ${size} [${fstype}]\nLabel: ${label}\nMount: ${mount}\n"
done < <(echo "$devices" | jq -c '.[]')
# Children (partitions)
while IFS= read -r line; do
if [ -z "$line" ] || [ "$line" = "null" ]; then continue; fi
local name size label mount fstype
name=$(echo "$line" | jq -r '.name')
size=$(echo "$line" | jq -r '.size')
label=$(echo "$line" | jq -r '.label // "N/A"')
mount=$(echo "$line" | jq -r '.mountpoint // "not mounted"')
fstype=$(echo "$line" | jq -r '.fstype // "?"')
if [ "$mount" != "not mounted" ]; then
count=$((count + 1))
fi
tooltip_lines="${tooltip_lines} └ ${name} ${size} [${fstype}]\n Label: ${label}\n Mount: ${mount}\n"
done < <(lsblk -Jbpno NAME,RM,SIZE,LABEL,MOUNTPOINT,FSTYPE,TRAN,MODEL 2>/dev/null \
| jq -c '[.blockdevices[] | select(.tran == "usb") | .children[]?] | .[]')
# Determine alt class
local alt="usb-none"
if [ "$count" -gt 0 ]; then
alt="usb-mounted"
elif [ -n "$tooltip_lines" ]; then
alt="usb-unmounted"
fi
# Build JSON output
local text icon
if [ "$count" -gt 0 ]; then
icon="󰋊"
text="${icon} ${count}"
elif [ -n "$tooltip_lines" ]; then
icon="󰋊"
text="${icon} !"
else
text=""
fi
# Escape for JSON
local tooltip_json
tooltip_json=$(echo -e "$tooltip_lines" | head -20 | jq -Rs '.')
if [ -n "$text" ]; then
printf '{"text":%s,"tooltip":%s,"alt":%s}\n' \
"$(jq -n --arg t "$text" '$t')" \
"$tooltip_json" \
"$(jq -n --arg a "$alt" '$a')"
else
echo '{"text":"","tooltip":"","alt":"usb-none"}'
fi
}
# --- main ----------------------------------------------------------------------
case "${1:-}" in
open) do_open ;;
eject) do_eject ;;
*) do_status ;;
esac
+26 -1
View File
@@ -25,4 +25,29 @@
- "journalctl --vacuum-time=7d"
register: maintenance_result
changed_when: "'deleted' in maintenance_result.stdout or 'Vacuuming' in maintenance_result.stdout"
failed_when: false
failed_when: false
- name: Disable unused system services
ansible.builtin.systemd_service:
name: "{{ item }}"
enabled: false
state: stopped
loop:
- bluetooth.service
- NetworkManager-wait-online.service
failed_when: false
- name: Remove orphaned dependencies (pacman -Rns, iterative)
ansible.builtin.shell: |
removed_any=0
while orphans=$(pacman -Qdtq 2>/dev/null) && [ -n "$orphans" ]; do
pacman -Rns $orphans --noconfirm
removed_any=1
done
if [ "$removed_any" -eq 1 ]; then
echo "removed_orphans"
else
echo "no_orphans"
fi
register: orphan_cleanup
changed_when: "'removed_orphans' in orphan_cleanup.stdout"
+11
View File
@@ -0,0 +1,11 @@
---
- name: Ensure PDF viewer and generation tools are installed
community.general.pacman:
name:
# Minimal PDF viewer (Wayland-compatible)
- zathura
- zathura-pdf-mupdf
# PDF generation from HTML/CSS
- python-weasyprint
state: present
tags: [pdf, viewer, generation]
@@ -0,0 +1,6 @@
---
# Role-local defaults for system-upgrade.
# Project-wide kernel/NVIDIA package lists live in ansible/vars/main.yml so
# they can be reused by future roles (e.g. hyprland) without re-declaration.
# Override here only if you need a different value when running this role
# stand-alone (ansible-playbook ... -e @custom-vars.yml).
@@ -0,0 +1,6 @@
---
# Handlers for system-upgrade role.
# Currently the role uses explicit gated tasks (when: ... .changed) rather
# than notify/handler chains, because every kernel/driver change must trigger
# both mkinitcpio AND grub-mkconfig in a strict order. If you later split
# tasks across files, prefer the standard notify pattern here.
+289
View File
@@ -0,0 +1,289 @@
---
# system-upgrade role — full system upgrade + kernel/NVIDIA preparation
# for the upcoming Hyprland/Wayland migration. Idempotent: a clean re-run
# on an already-upgraded box performs no destructive action.
#
# Sections:
# 1. Pre-flight checks (OS guard, free space, summary)
# 2. Full system upgrade (pacman -Syu) — added in Step 2
# 3. NVIDIA branch migration + kernel install — added in Step 3
# 4. Initramfs + GRUB refresh + final reboot notice — added in Step 4
#
# Run as:
# ansible-playbook ansible/workstation.yml --check --tags system-upgrade
# ansible-playbook ansible/workstation.yml --tags system-upgrade
# ---------------------------------------------------------------------------
# 1. Pre-flight checks
# ---------------------------------------------------------------------------
- name: Pre-flight - announce role start
debug:
msg:
- "system-upgrade: preparing baseline for Hyprland/Wayland migration"
- "kernels to ensure installed: {{ kernel_packages | join(', ') }}"
- "nvidia modules to ensure installed: {{ nvidia_kernel_modules | join(', ') }}"
- "nvidia legacy packages to remove: {{ nvidia_packages_to_remove | join(', ') }}"
tags: [system-upgrade, preflight]
- name: Pre-flight - capture free space on / (GiB)
shell: |
set -o pipefail
df --output=avail -BG / | tail -1 | tr -dc '0-9'
args:
executable: /bin/bash
register: root_free_gb
changed_when: false
check_mode: false
tags: [system-upgrade, preflight]
- name: Pre-flight - fail if free space on / < {{ min_free_root_gb }} GiB
fail:
msg: >-
Only {{ root_free_gb.stdout }} GiB free on /, need at least
{{ min_free_root_gb }} GiB before running pacman -Syu and installing
a second kernel. Run `paccache -rk2` and clear ~/.cache before retrying.
when: (root_free_gb.stdout | int) < (min_free_root_gb | int)
tags: [system-upgrade, preflight]
- name: Pre-flight - record currently running kernel
command: uname -r
register: running_kernel
changed_when: false
check_mode: false
tags: [system-upgrade, preflight]
- name: Pre-flight - summary
debug:
msg:
- "running kernel: {{ running_kernel.stdout }}"
- "free space on /: {{ root_free_gb.stdout }} GiB (min required {{ min_free_root_gb }})"
- "OS guard: os_type == '{{ os_type }}' (must be 'archlinux')"
tags: [system-upgrade, preflight]
# ---------------------------------------------------------------------------
# 2. Full system upgrade (pacman -Syu)
# ---------------------------------------------------------------------------
# Bring the box to current Manjaro Stable before touching kernels/drivers.
# `community.general.pacman` runs the equivalent of `pacman -Syu`:
# - update_cache: yes -> pacman -Sy
# - upgrade: yes -> pacman -Su
# It reports changed=True iff packages were actually upgraded, which we use
# below to gate the kernel/driver migration so an already-current box is a
# true no-op.
- name: Upgrade - full system upgrade (pacman -Syu)
community.general.pacman:
update_cache: yes
upgrade: yes
register: pacman_syu
tags: [system-upgrade, upgrade, pacman]
- name: Upgrade - summary
debug:
msg:
- "pacman -Syu changed: {{ pacman_syu.changed | default(false) }}"
- "packages touched: {{ (pacman_syu.packages | default([])) | length }}"
- "first 10 packages: {{ (pacman_syu.packages | default([]))[:10] | join(', ') }}"
tags: [system-upgrade, upgrade]
# ---------------------------------------------------------------------------
# 3. NVIDIA branch migration + kernel install
# ---------------------------------------------------------------------------
# Goal: end up with BOTH linux618 (LTS/Fallback) and linux70 (stable) installed, each
# with matching *-nvidia kernel modules on the 580xx branch.
# in the same transaction so the GPU never has a "no driver" window of more
# than ~1 second on a local pacman cache.
#
# Safety guarantees:
# - linux618 (currently running kernel) is in `kernel_packages`, never in
# `nvidia_packages_to_remove` — asserted below.
# - The remove step uses the list of 575xx packages that are *actually*
# installed, so re-running the role after a successful migration is a
# true no-op (LEGACY is empty -> pacman -Rdd is skipped).
# - The install step uses --needed so already-current packages are not
# touched a second time.
- name: Migration - safety assertion (Fallback kernel must NOT be removed)
assert:
that:
- "'linux618' in kernel_packages"
- "'linux618' not in nvidia_packages_to_remove"
- "kernel_packages | length > 0"
- "nvidia_kernel_modules | length > 0"
fail_msg: >-
Refusing to migrate: configuration would remove the running fallback kernel.
Check ansible/vars/main.yml: 'linux618' must stay in kernel_packages.
tags: [system-upgrade, migration]
- name: Migration - detect which legacy 575xx packages are currently installed
shell: |
set -o pipefail
pacman -Qq {{ nvidia_packages_to_remove | join(' ') }} 2>/dev/null || true
args:
executable: /bin/bash
register: nvidia_legacy_installed
changed_when: false
check_mode: false
tags: [system-upgrade, migration]
- name: Migration - check-mode dry-run (pacman -S --print)
shell: |
set -o pipefail
pacman -S --needed --print {{ (kernel_packages + nvidia_kernel_modules + nvidia_userspace_packages) | join(' ') }} 2>&1 | head -40 || true
args:
executable: /bin/bash
register: nvidia_dryrun
changed_when: false
check_mode: false
when: ansible_check_mode
tags: [system-upgrade, migration]
- name: Migration - check-mode preview output
debug:
msg:
- "legacy 575xx packages currently installed:"
- "{{ nvidia_legacy_installed.stdout_lines | default([]) }}"
- "pacman would install (first 40 lines of --print):"
- "{{ nvidia_dryrun.stdout_lines | default([]) }}"
when: ansible_check_mode
tags: [system-upgrade, migration]
- name: Migration - swap NVIDIA branch (575xx -> 580xx) and install kernels
shell: |
set -eo pipefail
LEGACY="{{ nvidia_legacy_installed.stdout_lines | default([]) | join(' ') }}"
if [ -n "$LEGACY" ]; then
echo ">>> dropping legacy 575xx packages: $LEGACY"
pacman -Rdd --noconfirm $LEGACY
else
echo ">>> no legacy 575xx packages present — skipping drop step"
fi
echo ">>> ensuring target packages are present (kernels + nvidia modules + userspace)"
pacman -S --needed --noconfirm {{ (kernel_packages + nvidia_kernel_modules + nvidia_userspace_packages) | join(' ') }}
args:
executable: /bin/bash
register: nvidia_migration
become: yes
when: not ansible_check_mode
# Detect REAL pacman activity (not our own echo lines): pacman prints
# progress lines like "(3/4) installing linux70" / "(1/4) upgrading foo" /
# "(2/4) removing nvidia-575xx-utils". Match exactly that format so re-runs
# on an already-current box report changed=false.
changed_when: >-
(nvidia_migration.stdout | default('')) is search('\([0-9]+/[0-9]+\)\s+(installing|upgrading|removing)\s')
tags: [system-upgrade, migration]
- name: Migration - verify all target packages are present after install
command: pacman -Q {{ item }}
loop: "{{ kernel_packages + nvidia_kernel_modules + nvidia_userspace_packages }}"
register: pkg_verify
changed_when: false
check_mode: false
failed_when: (pkg_verify.rc | default(0)) != 0 and not ansible_check_mode
tags: [system-upgrade, migration]
- name: Migration - recovery notice
debug:
msg:
- "Recovery if linux70 fails to boot:"
- " 1. Reboot, hold SHIFT (or ESC) at POST to open the GRUB menu."
- " 2. Pick 'Advanced options for Manjaro Linux' -> 'linux618-x86_64'."
- " 3. linux618 + linux618-nvidia (580xx) modules are kept intact"
- " so the fallback path is always bootable."
tags: [system-upgrade, migration]
# ---------------------------------------------------------------------------
# 4. Initramfs refresh + GRUB regenerate + final reboot notice
# ---------------------------------------------------------------------------
# Manjaro's pacman post-install hooks normally invoke mkinitcpio and update-grub
# automatically. We re-run them explicitly here as a belt-and-suspenders safety
# net: if a hook was missed (e.g. interrupted transaction), the system still
# ends up bootable. Both tasks are gated on "did pacman actually change
# anything?" so an idempotent re-run is a true no-op.
- name: Boot - rebuild initramfs for all installed kernels (mkinitcpio -P)
command: mkinitcpio -P
become: yes
register: mkinitcpio_result
when:
- not ansible_check_mode
- (pacman_syu.changed | default(false)) or (nvidia_migration.changed | default(false))
tags: [system-upgrade, boot, initramfs]
- name: Boot - regenerate GRUB config (-> /boot/grub/grub.cfg)
command: grub-mkconfig -o /boot/grub/grub.cfg
become: yes
register: grub_mkconfig_result
when:
- not ansible_check_mode
- (pacman_syu.changed | default(false)) or (nvidia_migration.changed | default(false))
tags: [system-upgrade, boot, grub]
- name: Boot - stat both initramfs images
stat:
path: "{{ item }}"
loop:
- /boot/initramfs-6.18-x86_64.img
- /boot/initramfs-7.0-x86_64.img
register: initramfs_stat
changed_when: false
check_mode: false
tags: [system-upgrade, boot, initramfs]
- name: Boot - assert both initramfs images are present after install
assert:
that:
- item.stat.exists
fail_msg: >-
Missing initramfs image: {{ item.item }} — investigate the mkinitcpio
output above before rebooting. linux618 path may still be usable
if only the linux70 image is missing.
loop: "{{ initramfs_stat.results }}"
loop_control:
label: "{{ item.item }}"
when: not ansible_check_mode
tags: [system-upgrade, boot, initramfs]
- name: Boot - assert GRUB menu lists both kernels (linux618 + linux70)
shell: |
# Manjaro grub.cfg uses 'Manjaro Linux (Kernel: X.Y.Z-MANJARO x64)' format,
# NOT 'linux618'/'linux70' package-name tokens. Count menuentries for each
# kernel family separately so we fail if one is missing (a simple total
# count would mask a missing kernel if the other family has 2+ entries).
K618=$(grep -cE "menuentry .*Kernel: 6\.18\." /boot/grub/grub.cfg)
K70=$(grep -cE "menuentry .*Kernel: 7\.0\." /boot/grub/grub.cfg)
echo "linux618 (Kernel: 6.18.x) menuentries: ${K618}"
echo "linux70 (Kernel: 7.0.x) menuentries: ${K70}"
if [ "${K618}" -lt 1 ] || [ "${K70}" -lt 1 ]; then
echo "FAIL: missing menuentry for one or both kernels" >&2
exit 1
fi
echo "OK: both kernels are listed in /boot/grub/grub.cfg"
args:
executable: /bin/bash
register: grub_menu_check
changed_when: false
check_mode: false
failed_when:
- grub_menu_check.rc != 0
- not ansible_check_mode
tags: [system-upgrade, boot, grub]
- name: Boot - final reboot notice
debug:
msg:
- "================================================================"
- "system-upgrade role finished successfully."
- ""
- "NEXT STEP: reboot and select 'linux70' from the GRUB menu."
- " 1. sudo reboot"
- " 2. At the GRUB menu, choose 'Advanced options for Manjaro Linux'"
- " -> 'Manjaro Linux on /dev/... linux70-x86_64'."
- " 3. After login, verify with:"
- " uname -r # expect 7.0.x"
- " nvidia-smi # expect driver 580.159.03 on GTX 1050 Ti"
- ""
- "Only AFTER a successful linux70 boot, run the upcoming Hyprland role."
- "================================================================"
tags: [system-upgrade, notice]
+181
View File
@@ -1,7 +1,188 @@
# 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
swap_file_size_gb: 16
workspace_root: /home/mw/infrastructure/mw-pfeddersheim-workstation
os_type: archlinux
pnpm_version: "25.6.1"
python_version: "3.14.3"
php_version: "8.5.3"
# ---------------------------------------------------------------------------
# system-upgrade role: kernel + NVIDIA preparation for Hyprland/Wayland
# ---------------------------------------------------------------------------
# Strategy: keep linux618 as fallback, add linux70 (7.0.x stable)
# as the new daily-driver kernel. Migrate NVIDIA from the pinned 575xx branch
# to the 580xx branch (NFB) for Pascal compatibility.
# See: docs/adr/0002-kernel-and-nvidia-strategy.md
kernel_packages:
- linux618
- linux618-headers
- linux70
- linux70-headers
nvidia_kernel_modules:
- linux618-nvidia-580xx
- linux70-nvidia-580xx
nvidia_userspace_packages:
- nvidia-580xx-utils
- lib32-nvidia-580xx-utils
- opencl-nvidia-580xx
- nvidia-580xx-settings
nvidia_packages_to_remove:
- linux612-nvidia-575xx
- nvidia-575xx-utils
- lib32-nvidia-575xx-utils
- nvidia-575xx-settings
- opencl-nvidia
# Pre-flight: refuse to proceed if free space on / drops below this many GiB.
# Current usage is ~87% so we keep this conservative.
min_free_root_gb: 5
# ---------------------------------------------------------------------------
# hyprland role: packages and configuration
# ---------------------------------------------------------------------------
hyprland_packages:
- hyprland
- hyprlock
- hypridle
- hyprshot
- hyprcursor
- grim
- slurp
- wl-clipboard
- satty
- yazi
- xdg-desktop-portal-hyprland
- waybar
- hyprlauncher
- dunst
- kitty
- cliphist
- uwsm
- brightnessctl
- pipewire
- pipewire-pulse
- wireplumber
- pavucontrol
- egl-wayland
- qt5-wayland
- qt6-wayland
- chafa
- 7zip
- resvg
- udisks2
# X11 packages to remove (Wayland-only environment)
# Tier 1: no dependents, no active use — ADR 0007
hyprland_packages_remove:
# X11 clipboard (replaced by wl-clipboard)
- xclip
- xsel
# X11 display managers (replaced by greetd+tuigreet)
- lightdm
- lightdm-slick-greeter
- lightdm-settings
- sddm
- sddm-kcm
- sddm-breath-theme
# X11 video drivers (not used; Xwayland renders via Mesa)
- xf86-video-amdgpu
- xf86-video-ati
- xf86-video-intel
- xf86-video-nouveau
- xf86-video-vesa
# X11 input driver (superseded by libinput)
- xf86-input-synaptics
# X11 tools (no X11 session exists)
- xterm
- xorg-twm
- xorg-xinit
- xorg-xkill
- x11vnc
- numlockx
- startup-notification
- xorg-server-xvfb
# Legacy wallpaper
- illyria-wallpaper
# Redundant NTP (systemd-timesyncd is active)
- ntp
# KDE screenshot (replaced by grim+slurp+grimblast)
- spectacle
# KDE pamac tray (paru is AUR helper; pamac not running)
- pamac-tray-icon-plasma
# Tier 2: KDE Plasma stack (replaced by Hyprland native tools) — ADR 0007
# Dolphin and kdegraphics-thumbnailers are kept (no plasma-workspace dep)
- 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 file manager replaced by yazi; kdegraphics-thumbnailers was dolphin-only
- dolphin
- dolphin-plugins
- kdegraphics-thumbnailers
# Tier 2b: KDE screen management (replaced by wlr-randr/hyprlock) — ADR 0007
- kscreen
- kscreenlocker
# Tier 3: no active use, no dependents — ADR 0007
# Ruby: entire stack including subpackages (no active Ruby usage)
- ruby
- ruby-abbrev
- ruby-base64
- ruby-bigdecimal
- ruby-bundled-gems
- ruby-bundler
- ruby-csv
- ruby-debug
- ruby-default-gems
- ruby-drb
- ruby-erb
- ruby-getoptlong
- ruby-irb
- ruby-matrix
- ruby-minitest
- ruby-mutex_m
- ruby-net-ftp
- ruby-net-imap
- ruby-net-pop
- ruby-net-smtp
- ruby-nkf
- ruby-observer
- ruby-power_assert
- ruby-prime
- ruby-racc
- ruby-rake
- ruby-rbs
- ruby-rdoc
- ruby-repl_type_completor
- ruby-resolv-replace
- ruby-rexml
- ruby-rinda
- ruby-rss
- ruby-stdlib
- ruby-syslog
- ruby-test-unit
- ruby-typeprof
- rubygems
# LOVE game engine (no .love files on disk)
- love
# X11 server (no longer required; xorg-xwayland is independent)
- xorg-server
- xf86-input-libinput
# ---------------------------------------------------------------------------
# greetd role: display manager - minimal TUI greeter for Wayland
# Replaced SDDM (2026-05-20) - see docs/adr/0003-sddm-to-greetd.md
# ---------------------------------------------------------------------------
display_manager_package: greetd
display_manager_greeter: greetd-tuigreet
display_manager_config: /etc/greetd/config.toml
+6
View File
@@ -0,0 +1,6 @@
---
# Minimal placeholder so ansible/workstation.yml `vars_files` loads cleanly.
# The `system-upgrade` role does not reference any secrets.
# To add real secrets, vault-encrypt this file:
# ansible-vault encrypt ansible/vars/secrets.yml
# and provide the password via `--vault-password-file ansible/vault_password`.
+9
View File
@@ -8,6 +8,15 @@
- vars/secrets.yml
roles:
- role: system-upgrade
tags: [system-upgrade]
when: os_type == 'archlinux'
- common
- dev-tools
- maintenance
- fonts
- pdf
- role: hyprland
tags: [hyprland]
- role: greetd
tags: [greetd]
+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
+47
View File
@@ -0,0 +1,47 @@
Hyprland Keybindings Cheat Sheet
================================
$mainMod = SUPER (Windows key) | Monitors: DP-1 | DVI-D-1 (rotated) | HDMI-A-1
APPS
SUPER+Q Terminal (kitty)
SUPER+B Browser (chromium)
SUPER+E File manager (dolphin)
SUPER+D App launcher (hyprlauncher)
WORKTIME SAVERS
SUPER+F Toggle fullscreen
SUPER+SPACE Toggle floating (popup window)
SUPER+TAB Cycle next window
SUPER+SHIFT+TAB Cycle previous window
SUPER+` Toggle scratchpad (special workspace)
SUPER+SHIFT+` Send window to scratchpad
SUPER+P Pseudo mode (tile/float hybrid)
SUPER+J Toggle split direction
HOME ROW NAVIGATION (HJKL)
SUPER+H/J/K/L Focus left/down/up/right (same as arrows)
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+ALT+H Shrink window width (-40)
SUPER+ALT+L Expand window width (+40)
SUPER+ALT+K Shrink window height (-40)
SUPER+ALT+J Expand window height (+40)
ARROW KEY NAVIGATION
SUPER+Left/Right/Up/Down Focus direction
SUPER+SHIFT+Left/Right/Up/Down Move window to adjacent tile/monitor
WINDOW MANAGEMENT
SUPER+C Close active window
SUPER+M Exit Hyprland (logout)
SUPER+CTRL+L Lock screen (hyprlock)
WORKSPACES
SUPER+1-9 Go to workspace N
SUPER+SHIFT+1-9 Move window to workspace N
SESSION SERVICES (systemd user units, uwsm-managed)
hyprpaper, waybar, dunst, hypridle, hyprlauncher, cliphist
All managed via ~/.config/systemd/user/*.service
+120
View File
@@ -0,0 +1,120 @@
---
description: >-
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
- kernel
- nvidia
- hyprland
- wayland
last_updated: '2026-06-17'
---
# ADR 2: Kernel and NVIDIA Driver Strategy for Hyprland/Wayland
## Status
Accepted (2026-05-18) — Updated (2026-06-17)
## Context
The workstation runs Manjaro Linux on AMD Ryzen 7 2700X with an NVIDIA
GeForce GTX 1050 Ti (Pascal, GP107) driving three monitors. The next
infrastructure plan switches the desktop from KDE/X11 to Hyprland on
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, 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 `580xx` modules (or vice
versa) breaks the driver at the next kernel boot.
The starting point at the time of this ADR (2026-05-18):
| Item | Version |
|---------------------------|------------------------|
| Running kernel | `linux612 6.12.77-1` |
| NVIDIA driver | `575.64.05-3` (575xx branch) |
| NVIDIA kernel module pkg | `linux612-nvidia-575xx 575.64.05-31` |
| `linux70` available in repo | `7.0.3-1` |
| `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 `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 `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`,
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.
4. **Encapsulate everything in an idempotent Ansible role**
(`ansible/roles/system-upgrade/`) that runs **before** `common`,
`dev-tools` and `maintenance`. It is invoked stand-alone with
`--tags system-upgrade` and is safe on every subsequent run.
## Rationale
- **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.
- **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 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
to roughly one second of cache I/O on a local SSD.
- **Idempotent role over shell script.** Matches the existing
`common`/`dev-tools`/`maintenance` pattern, supports `--check`, and
re-running after success is a true no-op (validated by gating every
mutating task on `pacman_syu.changed or nvidia_migration.changed`).
## Consequences
- **Two kernels on `/` partition.** Roughly +120 MB for `linux70` plus
~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 `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/driver pin bumps are one-line changes** in
`ansible/vars/main.yml` (`kernel_packages` /
`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.
+95
View File
@@ -0,0 +1,95 @@
---
description: >-
Replace SDDM (Qt-based graphical display manager) with greetd + tuigreet
for a minimal, Wayland-first login experience with zero heavy GUI dependencies.
status: accepted
date: 2026-05-20
replaces: SDDM as display-manager.service
---
# 0003: SDDM to greetd Migration
## Context
The workstation uses Hyprland as the primary Wayland compositor. SDDM was
pulled in as the display manager with significant Qt dependencies (~50+
transitive packages). For a single-user dev workstation running Hyprland,
this is unnecessary overhead — heavy, slow-booting, and over-engineered for
the use case.
SDDM provides: graphical login prompt, session selection, PAM authentication.
All three can be achieved with far lighter tools.
## Decision
Migrate from SDDM to greetd + tuigreet.
- **greetd**: minimal display manager daemon, designed for Wayland first-class.
- **tuigreet**: TUI greeter with time display, user remembering, session
discovery from `/usr/share/wayland-sessions/` and `/usr/share/xsessions/`.
### What changed
| Before | After |
|--------|-------|
| sddm.service (display-manager) | greetd.service (display-manager) |
| Qt5/Qt6 dependency chain | greetd + greetd-tuigreet (3 packages, 4.9MB) |
| /etc/sddm.conf | /etc/greetd/config.toml |
| Graphical login screen | Text-based TUI login screen |
| Heavy PAM surface | Standard PAM chain (system-local-login) |
### Ansible changes
- Added `greetd` role: installs packages, configures config.toml from template,
enables greetd as display-manager.service
- Hyprland task renamed: "register session in SDDM" -> "register Wayland session
desktop entry" (no functional change, just accurate naming)
- vars/main.yml: added `display_manager_package`, `display_manager_greeter`,
`display_manager_config` variables
### Configuration
```toml
[terminal]
vt = 1
[default_session]
command = "tuigreet --time --remember --asterisks --cmd Hyprland"
user = "greeter"
```
### Rationale
1. **Wayland-native**: greetd handles Wayland sessions cleanly without X
compatibility layers
2. **Minimal**: 3 packages vs Qt dependency chain; <5MB vs ~500MB
3. **Maintained**: Rust codebase, clean TOML config, Arch repos (no AUR)
4. **AI-agent friendly**: TOML config is trivially parseable/modifiable;
clean systemd service with structured journalctl output
5. **Simplicity principle**: maximizes work not done
## Consequences
### Positive
- Faster boot (no Qt/Xorg init)
- Smaller attack surface
- Less disk usage
- No Qt dependencies to maintain
- Clean systemd service management
### Negative
- No graphical login (only a limitation if multi-user GUI switching is needed)
- SDDM themes not available (not a requirement for this workstation)
- Tuigreet has fewer visual customization options
### Migration path back
```bash
sudo systemctl disable greetd
sudo systemctl enable sddm
sudo ln -sf /usr/lib/systemd/system/sddm.service /etc/systemd/system/display-manager.service
```
Reversible with zero data loss — the hyprland.desktop session entry is
unchanged and works with both DMs.
+72
View File
@@ -0,0 +1,72 @@
---
description: >-
Override the package-managed hyprland.desktop with a local copy that points
to start-hyprland (watchdog binary), ensuring Hyprland launches with proper
crash diagnostics, signal handling, and XDG portal support via uwsm.
status: accepted
date: 2026-05-21
---
# 0004: start-hyprland Watchdog Override
## Context
Hyprland 0.53+ ships `start-hyprland`, a watchdog binary that monitors the
compositor process, collects crash diagnostics (crash reports, memory dumps),
and ensures proper signal handling. When Hyprland detects it was not launched
via `start-hyprland`, it displays a banner warning:
> `⚠ Hyprland was started without start-hyprland. This is highly not
> recommended unless you are in a debugging environment.`
Our setup uses: greetd → tuigreet → `uwsm start -e -D Hyprland hyprland.desktop`
uwsm reads the `.desktop` file's `Exec=` field and spawns that binary inside
a systemd user session. The package-provided `hyprland.desktop` has
`Exec=Hyprland` (direct binary), bypassing the watchdog.
### Research (Dec 2025 Jan 2026)
- Hyprland discussion #12661: widespread report of this warning with UWSM.
- UWSM maintainer Vladimir-csp confirmed: "I'd recommend feeding
`hyprland.desktop` to uwsm" with the watchdog binary.
- fufexan (Hyprland maintainer) acknowledged the incompatibility; no upstream
fix landed as of Jan 2026.
- Community consensus: point `.desktop` Exec to `start-hyprland` via local
override to preserve uwsm session management while enabling the watchdog.
## Decision
Create a local override at `~/.local/share/wayland-sessions/hyprland.desktop`
pointing to `start-hyprland`.
### Why local override (not system-wide)?
- The system file (`/usr/share/wayland-sessions/hyprland.desktop`) is owned by
the `hyprland` package and gets overwritten on updates.
- `~/.local/share` has higher XDG_DATA_DIRS priority than `/usr/share`, so
greetd/tuigreet discover the override first without touching system files.
- Ansible manages the override idempotently.
### Why not modify greetd's command?
The greetd command (`uwsm start -e -D Hyprland hyprland.desktop`) references
`hyprland.desktop` generically. Changing the greeting command to
`start-hyprland` directly would bypass uwsm entirely. The override approach
keeps the uwsm session management intact while fixing the watchdog issue.
## Status
Implemented in Ansible role `hyprland/tasks/main.yml`:
- Task registers the override at `~/.local/share/wayland-sessions/hyprland.desktop`
- Uses `start-hyprland` as `Exec` and `TryExec`
- Sets correct ownership (`mw`) and permissions (`0644`)
- Tagged `[hyprland, session]` for idempotent runs
## Verification
After running `ansible-playbook ansible/workstation.yml`:
1. `cat ~/.local/share/wayland-sessions/hyprland.desktop` should show
`Exec=start-hyprland`
2. Next login: the warning banner should no longer appear.
3. `ps aux | grep start-hyprland` should show the watchdog parent process.
+120
View File
@@ -0,0 +1,120 @@
# ADR 0006: Chrome/Chromium Normalization and Cleanup
**Date:** 2026-05-22
**Status:** Accepted
**Deciders:** Michael Wegener
## Context
The workstation had accumulated significant Chrome/Chromium configuration drift over time:
- **Dual installation**: Both `google-chrome-stable` (148.0.7778.178) and orphaned `chromium` configs
- **Dead data directories**:
- `~/.config/chromium/` (1.4 GB of stale profile data)
- `~/.local/chrome-cdp/` (8.6 GB Chrome DevTools Protocol debug profile, no longer used)
- **Broken PWA desktop entries**: 13 non-functional `.desktop` files in `~/.local/share/applications/`
- **Orphaned packages**: `chromium-widevine` (DRM codec plugin for non-existent Chromium)
- **Dead shell integrations**: `BROWSER_CDP_URL` environment variable auto-population in `~/.bashrc`
- **Wayland compatibility**: `chrome-flags.conf` needed Wayland-specific flags for native Wayland rendering
This drift made it difficult to:
1. Understand which browser is authoritative
2. Maintain clean Ansible playbooks
3. Ensure consistent Wayland behavior
4. Avoid confusion from broken PWA shortcuts
## Decision
Standardize on **`google-chrome-stable`** as the sole browser and remove all Chromium-related artifacts:
### 1. Remove Dead Data Directories
- ✅ Delete `~/.config/chromium/` entirely (1.4 GB freed)
- ✅ Delete `~/.local/chrome-cdp/` entirely (8.6 GB freed)
### 2. Uninstall Orphaned Packages
-`pacman -R chromium-widevine` (17.41 MiB freed)
### 3. Delete Broken PWA Desktop Entries
- ✅ Removed 13 non-functional `.desktop` files from `~/.local/share/applications/`
- ✅ Deleted 4 PWA manifest directories from `~/.config/google-chrome/Default/Web Applications/Manifest Resources/`
### 4. Clean Shell Environment
- ✅ Removed `BROWSER_CDP_URL` auto-population (lines 140-141) from `~/.bashrc`
- Bash startup improved from ~60ms to ~3ms
### 5. Restore Wayland Configuration
- ✅ Ansible role `dev-tools` already manages `~/.config/chrome-flags.conf` with Wayland flags
- ✅ Playbook re-applied to restore `--ozone-platform=auto` and other Wayland optimizations
### 6. Update Ansible for Future Maintenance
- ✅ Added `cleanup` tasks to `dev-tools` role to remove known-dead packages/directories
- ✅ Documented cleanup procedures in CHANGELOG
## Consequences
### Positive
- **10 GB disk space recovered** on root filesystem
- **Clear browser choice**: Only `google-chrome-stable` is installed and managed
- **Faster shell startup**: Removed unnecessary `curl` call to `localhost:9222`
- **Clean desktop**: PWA shortcuts now only show functional applications
- **Wayland-native rendering**: Chrome uses `--ozone-platform=auto` for native Wayland
- **Maintainable configuration**: Ansible now includes cleanup tasks for future orphan removal
### Negative
- None significant. Google Chrome is the sole browser, reducing cognitive load.
### Neutral
- AWDI (A Web Developer Interface) tools that required CDP connections must now launch Chrome with `--remote-debugging-port=9222` manually if needed.
## Technical Details
### Current Browser Installation
```
google-chrome-stable 148.0.7778.178-1 (AUR: paru)
```
### Ansible Management
- **Package installation**: `ansible/roles/dev-tools/tasks/main.yml` (lines 112-120)
- **Chrome flags**: `ansible/roles/dev-tools/tasks/main.yml` (lines 122-135) with `--tags chrome`
- **Cleanup tasks**: `ansible/roles/dev-tools/tasks/main.yml` (lines 137-158) with `--tags cleanup`
### Files Affected
- **Deleted**:
- `~/.config/chromium/` (1.4 GB)
- `~/.local/chrome-cdp/` (8.6 GB)
- 13 `.desktop` files in `~/.local/share/applications/`
- 4 PWA manifest dirs in `~/.config/google-chrome/Default/Web Applications/Manifest Resources/`
- **Modified**:
- `~/.bashrc` (removed CDP_URL lines)
- `~/.config/chrome-flags.conf` (restored via Ansible)
- **Uninstalled**:
- `chromium-widevine` (17.41 MiB)
### Hyprland Integration
- `SUPER+B` binds to `uwsm app -- google-chrome-stable` (unchanged)
- Waybar CDP module removed (no longer needed without debug profile)
- Cheatsheet references updated to reflect Chrome-only workflow
## Alternatives Considered
### Keep Chromium Installed
- **Pros**: Open-source browser, no Google telemetry
- **Cons**: Two browsers to maintain, orphaned packages, confusing shortcuts
- **Decision**: Rejected. Google Chrome provides better Wayland support and is already the daily driver.
### Preserve `chrome-cdp` Profile
- **Pros**: Could be reactivated for debugging, Playwright/Puppeteer testing
- **Cons**: 8.6 GB wasted space, not used in 3+ months, on-demand launch is trivial
- **Decision**: Rejected. AWDI tools can start Chrome with `--remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debug` if needed.
### Automated PWA Scrubbing
- **Pros**: Could remove all PWA configs via Ansible
- **Cons**: Risk of removing actively-used PWAs (OnlyOffice, JetBrains Toolbox, etc.)
- **Decision**: Rejected. Manually scrubbed dead PWAs, left functional ones intact.
## References
- [ADR 0004: Start-Hyprland Override](0004-start-hyprland-override.md) — Wayland session management
- `ansible/roles/dev-tools/tasks/main.yml` — Chrome package and flags management
- `CHANGELOG.md` — 2026-05-22 cleanup entry
- `docs/tech/stack.md` — Current software audit (updated)
@@ -0,0 +1,141 @@
# ADR 0007: Workstation Package Cleanup — Remove X11/KDE Legacy Stack
**Date**: 2026-05-22
**Status**: Accepted (Tier 1+2+3+4 executed 2026-05-22)
**Decision Maker**: Michael Wegener
## Context
The workstation runs Hyprland (Wayland) with greetd+tuigreet. Despite this, 466 explicitly installed packages include ~40 that serve only X11 or KDE Plasma environments — neither of which are in active use. These packages waste disk space, increase update surface, and confuse package inventory.
### Current State
| Domain | Packages | Status |
|--------|----------|--------|
| X11 Display Managers | lightdm, lightdm-slick-greeter, sddm, sddm-kcm | Disabled; greetd is active DM |
| X11 Video Drivers | xf86-video-amdgpu, -ati, -intel, -nouveau, -vesa | Not used; Xwayland renders via Mesa |
| X11 Input Drivers | xf86-input-synaptics | Superseded by libinput |
| X11 Tools | xterm, xorg-twm, xorg-xinit, xorg-xkill, x11vnc, numlockx, startup-notification, xorg-server-xvfb | No X11 session exists |
| KDE/Plasma | kwin, plasma-desktop, plasma-workspace, plasma-nm, plasma-pa, plasma-x11-session, kdeplasma-addons, kde-gtk-config, powerdevil, spectacle, sddm-breath-theme, xdg-desktop-portal-kde, pamac-tray-icon-plasma | Not running; Hyprland equivalents in use |
| Legacy | illyria-wallpaper, ntp | Unused wallpaper; systemd-timesyncd already active |
### Wayland Equivalents Already In Use
| Function | KDE/X11 Package | Active Wayland Replacement |
|----------|----------------|--------------------------|
| Screenshots | spectacle | grim + slurp + grimblast |
| Network Applet | plasma-nm | waybar `network` module |
| Audio Applet | plasma-pa | waybar `pulseaudio` module + pavucontrol |
| Power Mgmt | powerdevil | hypridle + brightnessctl |
| Desktop Portal | xdg-desktop-portal-kde | xdg-desktop-portal-hyprland |
| Display Manager | lightdm/sddm | greetd + tuigreet |
| Clipboard | xclip/xsel | wl-clipboard |
| VNC | x11vnc | wayvnc (if needed) |
## Decision
### TIER 1 — Safe Removal (No Dependencies, No Active Use)
These packages have no dependents and no active role:
| # | Package | Reason | Size |
|---|---------|--------|------|
| 1 | lightdm | DM disabled, greetd is active | 1.1 MiB |
| 2 | lightdm-slick-greeter | Only required by lightdm | 1.9 MiB |
| 3 | sddm | DM disabled | 5.3 MiB |
| 4 | sddm-kcm | Only required by sddm | 0.6 MiB |
| 5 | sddm-breath-theme | Manjaro KDE branding for sddm | 0.2 MiB |
| 6 | xterm | X11 terminal; kitty is active | 1.1 MiB |
| 7 | xorg-twm | X11 window manager | 0.2 MiB |
| 8 | xorg-xinit | Start X manually (no X11 session) | 0.04 MiB |
| 9 | xorg-xkill | X11 kill utility | 0.02 MiB |
| 10 | x11vnc | Only works with X11; wayvnc for Wayland | 1.8 MiB |
| 11 | numlockx | X11-specific numlock | 0.02 MiB |
| 12 | startup-notification | X11 startup protocol | 0.06 MiB |
| 13 | xorg-server-xvfb | Virtual framebuffer; no CI use case | 2.0 MiB |
| 14 | xf86-video-amdgpu | AMD X11 driver (system has NVIDIA) | 0.2 MiB |
| 15 | xf86-video-ati | ATI X11 driver (no ATI GPU) | 0.5 MiB |
| 16 | xf86-video-intel | Intel X11 driver (no Intel GPU) | 2.2 MiB |
| 17 | xf86-video-nouveau | Nouveau X11 driver (NVIDIA proprietary in use) | 0.2 MiB |
| 18 | xf86-video-vesa | VESA fallback driver | 0.03 MiB |
| 19 | xf86-input-synaptics | X11 touchpad driver; libinput replaces it | 4.0 MiB |
| 20 | illyria-wallpaper | Manjaro wallpaper; not used | 1.0 MiB |
| 21 | ntp | systemd-timesyncd already active and running | ~2 MiB |
| 22 | spectacle | grim+slurp+grimblast already bound in Hyprland | 6.1 MiB |
| 23 | pamac-tray-icon-plasma | pamac not running; paru is AUR helper | 0.08 MiB |
**Tier 1 Savings: ~31 MiB** (plus dependency trees, likely 80-120 MiB total)
### TIER 2 — KDE Plasma Stack Removal (Cascade Removal)
These form a dependency chain. Removing plasma-workspace cascades:
| # | Package | Reason | Size |
|---|---------|--------|------|
| 24 | plasma-desktop | No KDE desktop in use | 39.3 MiB |
| 25 | plasma-workspace | Not running; waybar replaces | 55.1 MiB |
| 26 | kwin | Not running; Hyprland is compositor | 30.9 MiB |
| 27 | kdeplasma-addons | Requires kwin+plasma-workspace | 12.0 MiB |
| 28 | plasma-nm | waybar network module in use | 12.8 MiB |
| 29 | plasma-pa | waybar pulseaudio module in use | 1.9 MiB |
| 30 | plasma-x11-session | X11 session; Hyprland is active | 3.4 MiB |
| 31 | kde-gtk-config | KDE GTK integration; not needed | 0.3 MiB |
| 32 | powerdevil | hypridle replaces | 5.9 MiB |
| 33 | xdg-desktop-portal-kde | xdg-desktop-portal-hyprland in use | 2.4 MiB |
**Tier 2 Savings: ~164 MiB** (plus shared KDE deps, likely 250-400 MiB total)
**Important**: Dolphin and KDE apps that don't depend on plasma-workspace/kwin will remain installed. Dolphin's dependency chain only goes through KDE Frameworks (kio, baloo, etc.), not plasma-workspace.
### TIER 3 — Review Required (User Decision)
| # | Package | Notes | Question |
|---|---------|-------|----------|
| 34 | ruby | Explicitly installed; nothing visible uses it. ~50 ruby-* subpackages exist as deps. | Is anything using Ruby? |
| 35 | love | LÖVE 2D game engine. 4.1 MiB. No .love files found. | Still needed? |
| 36 | xorg-server | Required by lightdm+sddm (both being removed). xorg-xwayland is separate and will remain. | Remove after Tiers 1+2? |
| 37 | xf86-input-libinput | Required by xorg-server. If xorg-server goes, this can go too. | Depends on #36 |
### TIER 4 — Reclassify (Keep, Move to Correct Domain)
| Package | Current Domain | New Domain |
|---------|---------------|------------|
| uv | Misc | Python Toolchain |
| jq | Misc | Shell/Terminal |
| inxi | Misc | Hardware |
| lsof | Misc | Core OS |
| iproute2 | Misc | Network |
| perl-image-exiftool | Misc | Dev Tools |
| icoutils | Misc | Dev Tools |
| innoextract-git | Misc | Dev Tools |
| opencl-headers | Misc | GPU Compute |
| libxnvctrl-580xx | Misc | NVIDIA |
| zram-generator | Misc | Core OS |
| mkinitcpio-openswap | Misc | Core OS |
| openresolv | Misc | Network |
## Execution Plan
1. **Tier 1**: Remove 23 packages via Ansible + `pacman -Rns` (safe, no dependents)
2. **Tier 2**: Remove 10 KDE Plasma packages (cascade; verify dolphin survives)
3. **Tier 3**: Get user decisions, then act
4. **Tier 4**: Reclassify in Ansible vars (no removal, documentation only)
5. **Verify**: Run `ansible-playbook --check` after each tier
6. **Document**: Update `docs/tech/stack.md` and CHANGELOG.md
### Safety
- xorg-xwayland stays (required by Hyprland for X11 app compatibility)
- xf86-input-libinput stays if xorg-server stays (Tier 3 decision)
- Dolphin and all KDE Frameworks libs stay (no dependency on plasma-workspace)
- Backup: `pacman -Qe` snapshot before execution
## Expected Outcome
| Metric | Before | After |
|--------|--------|-------|
| Explicit packages | 466 | ~425-430 |
| Disk freed | — | 300-500 MiB |
| X11 surface area | 24 packages | 1-2 (xorg-xwayland + maybe xorg-server) |
| KDE Plasma | 13 packages | 0 |
| Update complexity | High | Reduced |
+150
View File
@@ -0,0 +1,150 @@
# ADR-00N: Migrate from KDE Wallet to gnome-keyring + Bitwarden
## Status
Implemented 2026-05-21. Post-reboot verification pending.
## Context
The system was previously configured as a KDE Plasma workstation with KWallet (kwalletd6) for secrets management. After the desktop environment was replaced with Hyprland (Wayland, greetd DM), KWallet no longer auto-unlocks (no PAM integration with greetd) and leaves apps without a functional D-Bus secrets service.
Apps requiring secrets (himalaya, ssh-agent, git, JetBrains IDEs) were failing to read credentials.
## Decision
### Architecture
```
┌────────────────────────
│ Bitwarden Vault │ ← Central, cross-device, backup
│ bitwarden.jantec.xyz │
│ (self-hosted) │
│ ▲ │
│ rbw CLI (Rust) │ ← CLI access, sync add/list
│ ironmikechw+bitwarden │
│ ▲ │
│ gnome-keyr │ ← Local encrypted cache, D-Bus API for app
│ libsecret + D-Bus │
│ org.freedesktop. │
│ secrets │
└────────────────────────┘
```
### Components
| Component | Role | Status |
|-----------|------|--------|
| **gnome-keyring** | Local D-Bus secrets provider. Started via systemd socket, enabled for all user sessions. | `enabled` + `active` |
| **rbw** | Rust Bitwarden CLI. Connects to `https://bitwarden.jantec.xyz`. Uses gnome-keyring as encrypted password store. | `1.15.0`, configured |
| **libsecret** | GTK/GLib secrets API. Used by git (`credential.helper libsecret`), secret-tool, and apps. | present |
| **himalaya** | Configured to use `secret-tool lookup email <addr>` for IMAP/SMTP auth.cmd. | needs password entries |
### What was migrated from KWallet
| Source (kwallet folder) | Entries | Destination |
|------------------------|---------|-------------|
| ksshaskpass (SSH keys) | id_ed25519 passphrase, id_ed25519_ja_manjaro passphrase | BW + GK |
| ksshaskpass (host passwords) | b0t@192.168.0.141 | BW + GK |
| ksshaskpass (tokens) | GitLab PAT, GitHub token | BW + GK |
| Remmina (5 entries) | VNC/RDP passwords for b0t-devops-box, auc-test, 192.168.0.52, ja-manjaro, vagrant | BW + GK |
| Nextcloud (3 entries) | mw@cloud.satware.com, mw@data.satware.com, allvater@cloud.makercloud.de | BW + GK |
| QGIS | Master password | BW + GK |
| JetBrains AI | AI token | BW + GK |
| JetBrains Account | Account token | BW only |
| Browser safe storage | Chrome, Brave, Chromium encryption keys | BW + GK |
| xdg-desktop-portal | Screen sharing tokens (18 binary entries) | skipped (app-generated) |
**Total: ~45 meaningful entries migrated**
### What was NOT migrated
| Item | Reason | Action needed |
|------|--------|--------------|
| himalaya email passwords | Were not stored in KWallet (probably in KDE KWallet but in a different format) | Must be added manually to GK via `secret-tool` |
| GitHub CLI token | Was in KWallet but may be expired | Re-auth with `gh auth login` |
| JetBrains IDE DB/SshConfigPassword | Encrypted app-internal tokens, app-specific | IDEs will re-create via D-Bus on next use |
### Config changes
| File | Change |
|------|--------|
| `~/.config/rbw/config.json` | **new** — rbw config pointing to bitwarden.jantec.xyz |
| `~/.config/systemd/user/default.target.wants/` | gnome-keyring-daemon.service + socket: **enabled** |
| `~/.gitconfig` | `credential.helper = libsecret` (changed from `store`) |
| `~/.config/systemd/user/` | gnome-keyring-daemon.service + socket: **enabled** |
### KDE Wallet cleanup
| State | Status |
|-------|--------|
| `kwallet` pkg (6.26.0) | Installed (will be removed post-verify) |
| `kwalletmanager` pkg (26.04.1) | Installed (will be removed post-verify) |
| `~/.local/share/kwalletd/kdewallet.kwl` | Still exists (backed up) |
| `~/.local/share/kwalletd/kdewallet_attributes.json` | Still exists (backed up)|
| `plasma-kwallet-pam.service` | inactive (static) |
PAM kwallet refs in `/etc/pam.d/*` were already commented out.
## Startup / Unlock flow after reboot
```
1. greetd/tuigreet → user login (PAM auth)
2. gnome-keyring PAM module (pam_gnome_keyring.so) unlocks keyring with login password
3. gnome-keyring-daemon starts via socket activation (systemd user)
4. D-Bus org.freedesktop.secrets becomes available
5. rbw auto-login (via pinentry + gnome-keyring) → vault accessible
6. himalaya can read IMAP passwords via secret-tool
7. ssh-add reads SSH passphrases via GK
8. git-credential-libsecret reads Git creds via GK
9. JetBrains IDEs read secrets via D-Bus auto-detection
```
## Post-reboot verification checklist
- [ ] `systemctl --user is-active gnome-keyring-daemon.service``active`
- [ ] `rbw list | wc -l` → entries available
- [ ] `secret-tool lookup application ssh-agent service ssh ssh-key /home/mw/.ssh/id_ed25519` → returns passphrase
- [ ] `secret-tool lookup application git service gitlab.satware.com` → returns PAT
- [ ] `himalaya account list` → accounts show, no auth errors
- [ ] `rbw get "SSH Key: id_ed25519"` → "SSH gitlab.satware.com"
- [ ] `git remote` operations work (libsecret helper)
- [ ] JetBrains IDEs can read DB/Git credentials
## Manual steps after reboot
1. If himalaya shows auth errors, re-enter IMAP passwords:
```bash
# MW account
echo "<IMAP_PASSWORD>" | secret-tool store --label="IMAP MW" email mw@satware.com application himalaya
# JA account
echo "<IMAP_PASSWORD>" | secret-tool store --label="IMAP JA" email ja@satware.ai application himalaya
# JA Gmail
echo "<IMAP_PASSWORD>" | secret-tool store --label="IMAP JA Gmail" email ja.satware@gmail.com application himalaya
# Michael Gmail
echo "<IMAP_PASSWORD>" | secret-tool store --label="IMAP Michael" email ironmikechw@gmail.com application himalaya
```
2. If GitHub CLI is stale:
```bash
gh auth login --hostname github.com
```
Then store in GK:
```bash
rbw get "GitHub Personal Token" | secret-tool store --label="GitHub Personal Token" \
application gh-cli service github.com
```
3. Remove KDE Wallet packages (after all verified working):
```bash
sudo pacman -Rns kwallet kwalletmanager
```
## Consequences
- **Positive**: Clean D-Bus secrets service available to all apps. Central vault syncs across devices. No more KWallet prompt on every login. libsecret works.
- **Negative**: Two layers of keyring (gnome-keyring local cache + Bitwarden vault). If gnome-keyring is corrupted, need `rbw sync`. If Bitwarden server is down, `rbw` won't work (but GK local cache still holds secrets).
## Rollback plan
If the migration fails, restore by:
1. Re-enable `plasma-kwallet-pam.service`
2. Revert `credential.helper` to `store`
3. KWallet data file remains on disk (not deleted)
+82
View File
@@ -0,0 +1,82 @@
# Hyprland Configuration - uwsm Integration
## Architecture
Session management uses **UWSM** (Universal Wayland Session Manager) wrapping Hyprland in systemd user services.
```
greetd → tuigreet → uwsm start → systemd user session → Hyprland
systemd units: hyprpaper, waybar, dunst, hypridle, hyprlauncher, cliphist
XDG autostart: Nextcloud, JetBrains Toolbox, Flameshot, etc.
```
## Session Services
All session services are managed via systemd user units (`~/.config/systemd/user/*.service`):
| Service | Purpose | Status |
|---------|---------|--------|
| `hyprpaper.service` | Wallpaper daemon | active |
| `waybar.service` | Status bar | active |
| `dunst.service` | Notification daemon | active |
| `hypridle.service` | Idle/screenlock control | active |
| `hyprlauncher.service` | App launcher daemon | active |
| `cliphist.service` | Clipboard history store | active |
### Management Commands
```bash
# Check all session services
systemctl --user list-units | grep -E 'hypr|waybar|dunst|cliphist'
# Restart a specific service
systemctl --user restart waybar.service
# View logs
journalctl --user -u waybar.service -f
```
## Launchers
| Component | Tool | Keybind |
|-----------|------|---------|
| App Launcher | hyprlauncher | SUPER+D |
| Keybind Menu | hyprlauncher -o (piped) | SUPER+CTRL+K |
hypr launcher config: `~/.config/hypr/hyprlauncher.conf`
- `desktop_launch_prefix = uwsm app --` (proper systemd scoping)
- Cache enabled for frequency-based sorting
- Finders: desktop (default), math (=), unicode (.), font (')
## Login Manager
**greetd** with **tuigreet** on VT 1.
Config: `/etc/greetd/config.toml`
Default command: `tuigreet --time --remember --asterisks --cmd 'uwsm start -e -D Hyprland hyprland.desktop'`
### Wayland Session Override (start-hyprland)
The package provides `/usr/share/wayland-sessions/hyprland.desktop` with
`Exec=Hyprland` (direct binary), but Hyprland 0.53+ requires launching through
`start-hyprland` (watchdog parent process) for crash diagnostics and proper
signal handling. Otherwise a warning banner appears on login.
A local override at `~/.local/share/wayland-sessions/hyprland.desktop` sets
`Exec=start-hyprland`, which uwsm then manages inside the systemd session.
XDG_DATA_DIRS priority ensures the override wins without touching system files.
Managed by Ansible role `hyprland` — see `docs/adr/0004-start-hyprland-override.md`.
## Key Categories
| Category | Modifier | Keys | Action |
|----------|----------|------|--------|
| Focus | SUPER | H/J/K/L, Arrows | Move focus |
| Move Window | SUPER+SHIFT | H/J/K/L, Arrows | Move window |
| Resize Window | SUPER+ALT | H/J/K/L | Resize active window |
| Windows | SUPER | F, SPACE, TAB, `, P, J | Window actions |
| Apps | SUPER | Q, B, E, D | Launch apps |
| Workspaces | SUPER | 1-9 | Switch/move workspaces |
| Lock | SUPER+CTRL | L | Lock screen |
See `~/.config/hypr/cheatsheet.txt` for the full reference.
@@ -1,33 +1,97 @@
# Learning: Enable WebGPU in Chrome on Linux (Manjaro)
---
description: >-
Chrome 148 on NVIDIA Wayland (Hyprland) — full GPU/video acceleration config.
Replaced the 2026-03-11 WebGPU flags with state-of-the-art NVIDIA VA-API setup.
tags: [chrome, nvidia, wayland, vaapi, gpu, acceleration]
last_updated: '2026-05-21'
---
Date: 2026-03-11
# Chrome 148 — NVIDIA Wayland Hardware Acceleration
Date: 2026-05-21 (updated from 2026-03-11)
## Context
WebGPU was reported as unavailable in Chrome despite the API being detected. On Linux, WebGPU support is often experimental and requires specific hardware acceleration flags.
## Challenge
- The Google Chrome binary on Manjaro (`/usr/bin/google-chrome-stable`) is a wrapper script that supports a custom flag file at `~/.config/chrome-flags.conf`.
- Enabling WebGPU requires not just the WebGPU flag, but also Vulkan support.
Chrome on NVIDIA + Wayland has three known issues:
1. **GBM "Permission denied"**: Chrome's seccomp sandbox blocks NVIDIA's `nvidia-drm_gbm.so` on Wayland.
2. **Vulkan/Wayland incompatibility**: `--ozone-platform=wayland` is not compatible with Vulkan rendering (Chromium issue 334275637).
3. **NVIDIA VA-API allowlist**: Hardware video decode is blocked on NVIDIA without explicit flags.
## Solution
Implemented a persistent configuration via Ansible to manage `~/.config/chrome-flags.conf` with the following flags:
The old WebGPU config (`--enable-unsafe-webgpu`, `--enable-vulkan`) is obsolete —
WebGPU works natively in Chrome 148 via ANGLE+OpenGL on Wayland without Vulkan flags.
```text
--enable-unsafe-webgpu
--enable-vulkan
--enable-features=Vulkan
--ignore-gpu-blocklist
## Required Packages
| Package | Reason |
|---------|--------|
| `libva-utils` | Verification: `vainfo` |
| `libva-nvidia-driver` | VA-API NVDEC bridge (already installed, was `nvidia-vaapi-driver`) |
Note: `libva-utils` was installed 2026-05-21. `libva-nvidia-driver` (0.0.17-1) was already present.
## Configuration: `~/.config/chrome-flags.conf`
```
--ozone-platform=wayland
--disable-seccomp-filter-sandbox
--disable-features=Vulkan,VulkanFromANGLE,DefaultANGLEVulkan
--use-angle=gl
--enable-features=VaapiOnNvidiaGPUs,VaapiIgnoreDriverChecks,AcceleratedVideoDecodeLinuxZeroCopyGL,AcceleratedVideoDecodeLinuxGL
--password-store=gnome-libsecret
```
## Key Learnings
1. **Persistent Flags**: On Arch-based systems (like Manjaro), use `~/.config/chrome-flags.conf` instead of modifying `.desktop` files, as it survives package updates and is automatically loaded by the Chrome wrapper script.
2. **WebGPU Dependencies**: WebGPU on Linux currently relies on the Vulkan backend. Enabling `--enable-unsafe-webgpu` alone is often insufficient; `--enable-vulkan` and `--enable-features=Vulkan` are required.
3. **GPU Blocklist**: Linux GPUs are often blocklisted for experimental features. `--ignore-gpu-blocklist` helps override these safety checks for development.
### Flag Rationale
| Flag | Purpose |
|------|---------|
| `--ozone-platform=wayland` | Force Wayland; auto-detect unreliable on Hyprland |
| `--disable-seccomp-filter-sandbox` | Fixes GBM `Permission denied` (Chrome sandbox blocks NVIDIA GBM on Wayland) |
| `--disable-features=Vulkan,VulkanFromANGLE,DefaultANGLEVulkan` | Vulkan incompatible with Ozone Wayland; all three must be disabled |
| `--use-angle=gl` | Force ANGLE→OpenGL backend; prevents auto-selection of Vulkan |
| `--enable-features=VaapiOnNvidiaGPUs,...` | Bypass NVIDIA video decode allowlist; modern flags (replaced deprecated `VaapiVideoDecoder`) |
| `--password-store=gnome-libsecret` | gnome-keyring integration (not auto-detected outside GNOME DE) |
## Verification
- Applied changes via Ansible role `dev-tools`.
- Verified file existence and content.
- Verified flag application by inspecting running processes (`ps aux`).
- Used Playwright (via MCP) to navigate to `https://webgpureport.org/`.
- Confirmed that while `navigator.gpu` is detected, `requestAdapter()` may still return null in headless/remote-controlled environments due to GPU access limitations.
- **SUCCESS**: Verified that the user's running Chrome instance successfully initializes WebGPU on the actual workstation hardware after the flag application, as confirmed by `https://webgpureport.org/`.
```bash
vainfo # Shows VA-API NVDEC decoder capabilities
LIBVA_DRIVER_NAME=nvidia vainfo # Explicit NVIDIA VA-API check
```
Expected `chrome://gpu` results:
- Canvas: **Hardware accelerated**
- Compositing: **Hardware accelerated**
- Video Decode: **Hardware accelerated** (H.264, VP9, HEVC)
- WebGL: **Hardware accelerated**
- WebGPU: **Hardware accelerated**
- GPU: GTX 1050 Ti / NVIDIA 580.159.03
- GL implementation: `gl=egl-angle,angle=opengl`
- GPU process crash count: **0**
- Ozone platform: **wayland**
## Known Warnings
Three log lines persist despite correct config — these are upstream Chromium warnings:
```
ERROR:ui/ozone/platform/wayland/gpu/wayland_surface_factory.cc:252: '--ozone-platform=wayland' is not compatible with Vulkan
WARNING:sandbox/policy/linux/sandbox_linux.cc:404: InitializeSandbox() called with multiple threads in process gpu-process
ERROR:ui/gl/egl_util.cc:92: EGL Driver message (Error) eglCreateContext: Requested version is not supported
```
These are cosmetic. Chrome falls back to ANGLE+OpenGL before they print.
No fix exists upstream yet (Chromium issue 334275637).
## Key Learnings
1. **Deprecated flags**: `VaapiVideoDecoder`, `VaapiVideoDecodeLinuxGL` replaced by `AcceleratedVideoDecodeLinuxZeroCopyGL`, `AcceleratedVideoDecodeLinuxGL` since Chrome ≥131.
2. **Vulkan is opt-in, not opt-out**: `--disable-features=Vulkan` alone is insufficient; `VulkanFromANGLE` and `DefaultANGLEVulkan` are auto-enabled and must be listed.
3. **GBM "Permission denied" is not file perms**: It's seccomp sandbox + NVIDIA GBM. `--disable-seccomp-filter-sandbox` is the confirmed workaround.
4. **Password store**: Hyprland ≠ GNOME, so `--password-store=gnome-libsecret` explicit flag needed.
5. **`--ignore-gpu-blocklist` no longer needed**: Chrome 148 recognizes the GTX 1050 Ti + current driver without it.
## Ansible Integration
The `dev-tools` role manages this file. Update both:
- `ansible/roles/dev-tools/tasks/main.yml` (the copy task content)
- This learning doc (the rationale)
@@ -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`
+1
View File
@@ -0,0 +1 @@
/.quarto/
+14
View File
@@ -0,0 +1,14 @@
format:
typst-pdf:
default: true
toc: false
number-sections: true
mainfont: "Inter"
monofont: "JetBrains Mono"
fontsize: 9pt
papersize: a4
margin:
top: 1.2cm
bottom: 1.2cm
left: 1.5cm
right: 1.5cm
+128
View File
@@ -0,0 +1,128 @@
/* Cheatsheet CSS for Markdown → HTML → PDF via WeasyPrint */
/* Clean, professional tables with Inter + JetBrains Mono */
@page {
size: a4 portrait;
margin: 10mm 14mm 10mm 14mm;
@bottom-center {
content: "Seite " counter(page);
font-size: 7pt;
color: #999;
}
}
* { margin-top: 0; }
body {
font-family: "Inter", "DejaVu Sans", sans-serif;
font-size: 9pt;
line-height: 1.3;
color: #111;
}
h1 {
font-size: 14pt;
font-weight: 700;
margin-bottom: 4pt;
padding-bottom: 4pt;
border-bottom: 1.5pt solid #111;
}
h2 {
font-size: 10.5pt;
font-weight: 700;
margin: 10pt 0 4pt 0;
color: #003366;
border-bottom: 0.5pt solid #ccc;
}
h3 {
font-size: 9pt;
font-weight: 600;
margin: 6pt 0 2pt 0;
color: #003366;
}
p { margin: 2pt 0; }
/* ---- Tables: clean reference style ---- */
table {
width: 100%;
border-collapse: collapse;
margin: 2pt 0 6pt 0;
font-size: 8pt;
page-break-inside: avoid;
}
th {
background: #f0f4f8;
font-weight: 700;
font-size: 7.5pt;
text-transform: uppercase;
letter-spacing: 0.04em;
color: #003366;
padding: 3pt 5pt;
border: none;
border-bottom: 1pt solid #0066cc;
text-align: left;
}
td {
padding: 2pt 5pt;
border: none;
border-bottom: 0.5pt solid #e4e4e4;
vertical-align: top;
}
tbody tr:nth-child(even) td {
background: #f7f9fb;
}
tbody tr:last-child td {
border-bottom: none;
}
/* ---- Code / key rendering ---- */
code, kbd {
font-family: "JetBrains Mono", monospace;
font-size: 8pt;
background: #f5f5f5;
padding: 1pt 3pt;
border-radius: 2pt;
border: 0.5pt solid #ddd;
}
pre {
font-family: "JetBrains Mono", monospace;
font-size: 7.5pt;
background: #f5f5f5;
padding: 4pt 6pt;
border-radius: 3pt;
border: 0.5pt solid #ddd;
page-break-inside: avoid;
white-space: pre-wrap;
}
/* ---- Two-column cheatsheet layout ---- */
.twocol {
column-count: 2;
column-gap: 14pt;
}
.twocol h2 {
break-before: column;
column-break-before: always;
}
.twocol h2:first-child {
break-before: avoid;
column-break-before: avoid;
}
hr {
border: none;
border-top: 0.5pt solid #ddd;
margin: 6pt 0;
}
a { color: #0066cc; text-decoration: none; }
@@ -0,0 +1,126 @@
---
title: "Hyprland Keybindings Cheatsheet"
author: "mw-pfeddersheim-workstation"
date: "May 2026"
---
# Hyprland Keybindings — Full Cheatsheet
**Layout:** German (de) | **Mod key:** SUPER (Windows key) | **Source:** `hyprctl binds` live
---
## General
| Shortcut | Action | Details |
|----------|--------|---------|
| `SUPER+Q` | Open terminal | kitty |
| `SUPER+C` | Kill active window | killactive |
| `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@144 |
| 2 | DVI-D-1 (BenQ BL2405) | Center, 1920x1080@60 rotated 90deg |
| 3 | HDMI-A-1 (TERRA 3280W) | Right, 2560x1440@144 |
## 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,200 @@
#set document(title: "Hyprland Keybindings - Pandoc")
#set text(font: ("Inter", "DejaVu Sans"), size: 9pt, lang: "de")
#set page(paper: "a4", margin: 1.2cm)
#set heading(numbering: "1.")
#set par(leading: 0.45em)
#show link: set text(navy)
#let horizontalrule = h(25%) + line(stroke: 0.5pt, length: 50%)
= Hyprland Keybindings --- Full Cheatsheet
<hyprland-keybindings-full-cheatsheet>
#strong[Layout:] German (de) | #strong[Mod key:] SUPER (Windows key) |
#strong[Source:] `hyprctl binds` live
#horizontalrule
== General
<general>
#figure(
align(center)[#table(
columns: (37.04%, 29.63%, 33.33%),
align: (auto,auto,auto,),
table.header([Shortcut], [Action], [Details],),
table.hline(),
[`SUPER+Q`], [Open terminal], [kitty],
[`SUPER+C`], [Kill active window], [killactive],
[`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+` #raw("`") (Grave)], [Toggle
scratchpad], [togglespecialworkspace],
[`SUPER+SHIFT+` #raw("`");], [Send window to
scratchpad], [movetoworkspace special],
[`SUPER+CTRL+K`], [Keybind cheat menu], [#emph[(broken --- pipelines
to grep+fuzzel)];],
[`SUPER+CTRL+L`], [Lock screen], [hyprlock],
[`SUPER+B`], [Web browser], [google-chrome-stable],
)]
, kind: table
)
== Navigation / Focus
<navigation-focus>
#figure(
align(center)[#table(
columns: 2,
align: (auto,auto,),
table.header([Shortcut], [Action],),
table.hline(),
[`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],
)]
, kind: table
)
== Window Movement
<window-movement>
#figure(
align(center)[#table(
columns: 2,
align: (auto,auto,),
table.header([Shortcut], [Action],),
table.hline(),
[`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],
)]
, kind: table
)
== Window Resize
<window-resize>
#figure(
align(center)[#table(
columns: 2,
align: (auto,auto,),
table.header([Shortcut], [Action],),
table.hline(),
[`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],
)]
, kind: table
)
== Screenshots
<screenshots>
#figure(
align(center)[#table(
columns: (41.67%, 33.33%, 25%),
align: (auto,auto,auto,),
table.header([Shortcut], [Action], [Tool],),
table.hline(),
[`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],
)]
, kind: table
)
== Workspaces
<workspaces>
#figure(
align(center)[#table(
columns: (55.56%, 44.44%),
align: (auto,auto,),
table.header([Shortcut], [Action],),
table.hline(),
[`SUPER+1..9`], [Switch to workspace 1-9],
[`SUPER+SHIFT+1..9`], [Move active window to workspace 1-9],
[`SUPER+` #raw("`");], [Toggle scratchpad (togglespecialworkspace)],
[`SUPER+SHIFT+` #raw("`");], [Move to scratchpad (movetoworkspace
special)],
)]
, kind: table
)
=== Workspace Layout
<workspace-layout>
#figure(
align(center)[#table(
columns: 3,
align: (auto,auto,auto,),
table.header([Workspace], [Monitor], [Position],),
table.hline(),
[1], [DP-1 (TERRA 3280W)], [Left, 2560x1440\@144],
[2], [DVI-D-1 (BenQ BL2405)], [Center, 1920x1080\@60 rotated 90deg],
[3], [HDMI-A-1 (TERRA 3280W)], [Right, 2560x1440\@144],
)]
, kind: table
)
== Notes
<notes>
- `SUPER+CTRL+K` (keybind menu) does #strong[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
<troubleshooting>
#figure(
align(center)[#table(
columns: 2,
align: (auto,auto,),
table.header([Issue], [Check],),
table.hline(),
[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`],
)]
, kind: table
)
== Satty Editor Shortcuts
<satty-editor-shortcuts>
#figure(
align(center)[#table(
columns: 2,
align: (auto,auto,),
table.header([Action], [Shortcut],),
table.hline(),
[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],
)]
, kind: table
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,193 @@
= Hyprland Keybindings --- Full Cheatsheet
<hyprland-keybindings-full-cheatsheet>
#strong[Layout:] German (de) | #strong[Mod key:] SUPER (Windows key) |
#strong[Source:] `hyprctl binds` live
#horizontalrule
== General
<general>
#figure(
align(center)[#table(
columns: (37.04%, 29.63%, 33.33%),
align: (auto,auto,auto,),
table.header([Shortcut], [Action], [Details],),
table.hline(),
[`SUPER+Q`], [Open terminal], [kitty],
[`SUPER+C`], [Kill active window], [killactive],
[`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+` #raw("`") (Grave)], [Toggle
scratchpad], [togglespecialworkspace],
[`SUPER+SHIFT+` #raw("`");], [Send window to
scratchpad], [movetoworkspace special],
[`SUPER+CTRL+K`], [Keybind cheat menu], [#emph[(broken --- pipelines
to grep+fuzzel)];],
[`SUPER+CTRL+L`], [Lock screen], [hyprlock],
[`SUPER+B`], [Web browser], [google-chrome-stable],
)]
, kind: table
)
== Navigation / Focus
<navigation-focus>
#figure(
align(center)[#table(
columns: 2,
align: (auto,auto,),
table.header([Shortcut], [Action],),
table.hline(),
[`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],
)]
, kind: table
)
== Window Movement
<window-movement>
#figure(
align(center)[#table(
columns: 2,
align: (auto,auto,),
table.header([Shortcut], [Action],),
table.hline(),
[`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],
)]
, kind: table
)
== Window Resize
<window-resize>
#figure(
align(center)[#table(
columns: 2,
align: (auto,auto,),
table.header([Shortcut], [Action],),
table.hline(),
[`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],
)]
, kind: table
)
== Screenshots
<screenshots>
#figure(
align(center)[#table(
columns: (41.67%, 33.33%, 25%),
align: (auto,auto,auto,),
table.header([Shortcut], [Action], [Tool],),
table.hline(),
[`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],
)]
, kind: table
)
== Workspaces
<workspaces>
#figure(
align(center)[#table(
columns: (55.56%, 44.44%),
align: (auto,auto,),
table.header([Shortcut], [Action],),
table.hline(),
[`SUPER+1..9`], [Switch to workspace 1-9],
[`SUPER+SHIFT+1..9`], [Move active window to workspace 1-9],
[`SUPER+` #raw("`");], [Toggle scratchpad (togglespecialworkspace)],
[`SUPER+SHIFT+` #raw("`");], [Move to scratchpad (movetoworkspace
special)],
)]
, kind: table
)
=== Workspace Layout
<workspace-layout>
#figure(
align(center)[#table(
columns: 3,
align: (auto,auto,auto,),
table.header([Workspace], [Monitor], [Position],),
table.hline(),
[1], [DP-1 (TERRA 3280W)], [Left, 2560x1440\@144],
[2], [DVI-D-1 (BenQ BL2405)], [Center, 1920x1080\@60 rotated 90deg],
[3], [HDMI-A-1 (TERRA 3280W)], [Right, 2560x1440\@144],
)]
, kind: table
)
== Notes
<notes>
- `SUPER+CTRL+K` (keybind menu) does #strong[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
<troubleshooting>
#figure(
align(center)[#table(
columns: 2,
align: (auto,auto,),
table.header([Issue], [Check],),
table.hline(),
[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`],
)]
, kind: table
)
== Satty Editor Shortcuts
<satty-editor-shortcuts>
#figure(
align(center)[#table(
columns: 2,
align: (auto,auto,),
table.header([Action], [Shortcut],),
table.hline(),
[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],
)]
, kind: table
)
Binary file not shown.
@@ -0,0 +1,126 @@
---
title: "Hyprland Keybindings Cheatsheet"
author: "mw-pfeddersheim-workstation"
date: "May 2026"
---
# Hyprland Keybindings — Full Cheatsheet
**Layout:** German (de) | **Mod key:** SUPER (Windows key) | **Source:** `hyprctl binds` live
---
## General
| Shortcut | Action | Details |
|----------|--------|---------|
| `SUPER+Q` | Open terminal | kitty |
| `SUPER+C` | Kill active window | killactive |
| `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@144 |
| 2 | DVI-D-1 (BenQ BL2405) | Center, 1920x1080@60 rotated 90deg |
| 3 | HDMI-A-1 (TERRA 3280W) | Right, 2560x1440@144 |
## 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 |
+7
View File
@@ -0,0 +1,7 @@
#set document(title: "Hyprland Keybindings - Pandoc")
#set text(font: ("Inter", "DejaVu Sans"), size: 9pt, lang: "de")
#set page(paper: "a4", margin: 1.2cm)
#set heading(numbering: "1.")
#set par(leading: 0.45em)
#show link: set text(navy)
#let horizontalrule = h(25%) + line(stroke: 0.5pt, length: 50%)
+18
View File
@@ -0,0 +1,18 @@
#set document(title: $if(title)$$title$$else$""$endif$)
#set text(font: ($if(mainfont)$"$mainfont"$else$"Inter"$endif$, "DejaVu Sans"), size: $if(fontsize)$$fontsize$$else$9pt$endif$, lang: $if(lang)$$lang$$else$en$endif$)
#set page(paper: "a4", margin: $if(margin-top)$($margin-top$,$else$1.5cm,$endif$ $if(margin-bottom)$$margin-bottom$,$else$1.5cm,$endif$ $if(margin-left)$$margin-left$,$else$1.5cm,$endif$ $if(margin-right)$$margin-right,$else$1.5cm$endif$)$else$1.5cm$endif$)
$if(number-sections)$#set heading(numbering: "1.")$endif$
$for(include-before)$
$include-before$
$endfor$
$body$
$for(include-after)$
$include-after$
$endfor$
$if(quotes)$#show link.label => { box(text(str: #quotes.open) + content + text(str: #quotes.close)) }$endif$
Binary file not shown.
@@ -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).
+87
View File
@@ -0,0 +1,87 @@
# Next Steps - 2026-05-21
## Immediate (Post-Reboot - Password Management Migration)
> Gnome-keyring + Bitwarden migration applied 2026-05-21. Verifying PAM unlock + D-Bus secrets service after reboot.
### Verification Checklist
- [ ] **Gnome-keyring auto-start**: `systemctl --user is-active gnome-keyring-daemon.service``active`
- [ ] **Rbw vault access**: `rbw unlocked && rbw list | head -5`
- [ ] **GK secret lookups work**:
```bash
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 application nextcloud service cloud.satware.com user mw
```
### Manual Steps Required After Reboot
**1. himalaya IMAP passwords** — Were NOT in KWallet, need manual entry.
Look up each password in https://bitwarden.jantec.xyz/ then store in GK:
```bash
# MW account (mw@satware.com)
echo "<PASSWORD>" | secret-tool store --label="IMAP MW" email mw@satware.com application himalaya
# JA account (ja@satware.ai)
echo "<PASSWORD>" | secret-tool store --label="IMAP JA" email ja@satware.ai application himalaya
# JA Gmail (ja.satware@gmail.com)
echo "<PASSWORD>" | secret-tool store --label="IMAP JA Gmail" email ja.satware@gmail.com application himalaya
# Michael Gmail (ironmikechw@gmail.com)
echo "<PASSWORD>" | secret-tool store --label="IMAP Michael" email ironmikechw@gmail.com application himalaya
```
**2. GitHub CLI** — Re-auth if token is stale:
```bash
gh auth login --hostname github.com
```
Then store token in GK
```bash
rbw get "GitHub Personal Token" --full | secret-tool store --label="GitHub Token" application gh-cli service github.com
```
### Verification Tests
| Area | Test Command | Expected Result |
|------|-------------|-----------------|
| Gnome-keyring | `systemctl --user is-active gnome-keyring-daemon.service` | `active` |
| Bitwarden sync | `rbw unlocked && rbw list | wc -l` | `true` + 397 entries |
| SSH passphrases | `secret-tool lookup application ssh-agent service ssh ssh-key /home/mw/.ssh/id_ed25519` | Returns "SSH gitlab.satware.com" |
| GitLab PAT | `secret-tool lookup application git service gitlab.satware.com` | Returns PAT |
| GitHub CLI | `gh auth status -h github.com` | Valid token (or needs re-auth) |
| himalaya | `himalaya -a mw check` | SMTP/IMAP OK |
| Git remotes | `git fetch` on any tracked repo | No password prompt |
| JetBrains IDEs | Open CLion/WebStorm/IDEA | No credential prompts (auto-detects D-Bus secrets) |
### Cleanup (After All Verified Working)
```bash
sudo pacman -Rns kwallet kwalletmanager
rm -rf ~/.local/share/kwalletd/
```
---
*Last updated: 2026-05-21 | Password management migration complete. Post-reboot verification pending.*
---
## Short Term (Non-Reboot)
- [ ] Verify that `archlinux-keyring-wkd-sync.service` failure is resolved or investigate further.
- [ ] Evaluate if `systemd-oomd` is aggressive enough for interactive desktop use; if not, consider testing `earlyoom` as an alternative.
- [ ] Automate periodic S.M.A.R.T. checks via a systemd timer (currently manual).
- [ ] Monitor system behavior under multi-day sustained load to ensure `zswap` and `MGLRU` keep the system responsive.
## Infrastructure
- [ ] Consolidate Ansible variables for Docker memory limits into `ansible/vars/main.yml`.
- [ ] Extend `scripts/maintenance.sh` to include a summary of the last 24h OOM events (if any).
- [ ] Verify that `archlinux-keyring-wkd-sync.service` failure is resolved or investigate further.
---
*See `docs/product/passwords-migration-checklist.md` for detailed post-reboot actions.*
*See `docs/product/passwords.md` for the password management architecture overview.*
*See `docs/adr/password-management-migration.md` for the Architecture Decision Record.*
+187
View File
@@ -0,0 +1,187 @@
# Password Management Infrastructure
## Architecture
```
┌─────────────────────────────────────────────────────────┐
│ Bitwarden Vault ───────────╮ │
│ bitwarden.jantec.xyz │ Central store │
│ (cloud) │ + backup │
│ │ │
│ ┌─────────────────┐ │ │
│ │ rbw CLI ─────┼─── sync/login ───┘ │
│ │ (Rust client) │ Add/edit/search │
│ └────────────────┘ │
│ │ │
│ ┌────────V──────── ┌──────────────────┐ │
│ │ gnome-keyring │────────────►│ Applications │ │
│ │ (libsecret) │ D-Bus │ himalaya, git, │ │
│ │ org.freedesktop.│ Secrets API │ SSH, browsers, │ │
│ │ secrets │ │ IDEs │ │
│ └─────────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
## Components
### 1. gnome-keyring (local D-Bus secret service)
- Provides `org.freedesktop.secrets` D-Bus interface
- Started as systemd user service via socket activation
- Unlocks on login (PAM integration with greetd/tuigreet)
- Stores credentials that apps access via `libsecret` (`secret-tool`)
| Unit | Status |
|------|--------|
| `~/.config/systemd/user/gnome-keyring-daemon.socket` | enabled |
| `~/.config/systemd/user/gnome-keyring-daemon.service` | enabled |
### 2. rbw (Bitwarden CLI)
- Unofficial Bitwarden CLI written in Rust
- Connects to `https://bitwarden.jantec.xyz`
- Uses gnome-keyring as its encrypted password store (via `pinentry`)
- Manages vault: `rbw add`, `rbw get`, `rbw list`, `rbw search`, `rbw sync`
| Config | Value |
|--------|-------|
| `~/.config/rbw/config.json` | `{"email": "ironmikechw+bitwarden@gmail.com", "base_url": "https://bitwarden.jantec.xyz"}` |
| API Keys | Stored in `~/.config/rbw/` (registered via `rbw register`) |
| Vault sync | Automatic via `rbw sync` (interval: 3600s) |
### 3. SSH Key Passphrases
- Stored in gnome-keyring (libsecret) for `ssh-add` via `gnome-keyring-ssh` PAM module
- Passphrases stored under `application=ssh-agent`, `service=ssh`, `ssh-key=<path>`
- Keys migrated from KWallet: `id_ed25519`, `id_ed25519_ja_manjaro`
### 4. Git Credentials
- **Global helper**: `libsecret` (stores in gnome-keyring instead of `~/.git-credentials` plaintext)
- **GitHub**: `gh auth git-credential` helper (for github.com)
- **GitLab**: configured as `provider = generic` in `.gitconfig` for gitlab.satware.com
### 5. Application Credentials Map
| Application | Storage | Mechanism |
|-----------|---------|-----------|
| himalaya (email) | gnome-keyring + Bitwarden | `secret-tool lookup email <address>` |
| ssh-agent | gnome-keyring | D-Bus secret service |
| git (global) | gnome-keyring (`libsecret`) | `git config credential.helper libsecret` |
| GitHub CLI | Bitwarden + gnome-keyring | `rbw get "GitHub Personal Token"` + GK entry |
| GitLab | gnome-keyring | `secret-tool lookup application git service gitlab.satware.com` |
| Chrome/Brave/Chromium | gnome-keyring | Safe Storage encryption keys |
| JetBrains IDEs | gnome-keyring | D-Bus secret service auto-detection |
| Remmina | gnome-keyring + Bitwarden | VNC/RDP passwords |
| Nextcloud | gnome-keyring + Bitwarden | Instance passwords |
| QGIS | gnome-keyring + Bitwarden | Master password |
## Startup / Unlock Flow
```
1. greetd/tuigreet login → PAM auth
2. PAM → gnome-keyring PAM module (pam_gnome_keyring.so)
3. Keyring unlocked with login password
4. gnome-keyring-daemon starts (socket activation)
5. D-Bus org.freedesktop.secrets available
6. rbw login → decrypts vault via gnome-keyring
7. himalaya can read IMAP passwords via secret-tool
8. ssh-add can read SSH passphrases via GK
9. git-credential-libsecret reads Git creds via GK
```
## Migration History
- **Before**: KDE Wallet (kwalletd) — no auto-unlock under greetd, no D-Bus support for non-Plasma apps
- **After**: gnome-keyring + rbw + Bitwarden.vault → full D-Bus secret service + central vault + CLI access
## Manual Entry Required
### himalaya Email Passwords
The following email account passwords need to be stored in gnome-keyring (they were NOT in KWallet):
```bash
# Store MW account
echo "<PASSWORD>" | secret-tool store --label="IMAP MW" email mw@satware.com application himalaya
# Store JA account
echo "<PASSWORD>" | secret-tool store --label="IMAP JA" email ja@satware.ai application himalaya
# Store JA Gmail
echo "<PASSWORD>" | secret-tool store --label="IMAP JA Gmail" email ja.satware@gmail.com application himalaya
# Store Michael Gmail
echo "<PASSWORD>" | secret-tool store --label="IMAP Michael Gmail" email ironmikechw@gmail.com application himalaya
```
## Useful Commands
```bash
# Bitwarden operations
rbw list # list all vault entries
rbw search <term> # search vault
rbw get <name> # get password (truncates on display)
rbw get --full "GitHub Personal Token" # get full password (piped)
rbw sync # sync with vault
rbw add "Service Name" # add new entry (opens $EDITOR)
# gnome-keyring operations
secret-tool lookup email mw@satware.com # lookup by attribute
secret-tool search application ssh-agent # search
secret-tool store --label="Test" key value # store (pipe data)
dbus-send --session --print-reply --dest=org.freedesktop.secrets \
/org/freedesktop/secrets/collection/default # low-level D-Bus
# Verify startup
systemctl --user status gnome-keyring-daemon.socket
systemctl --user status gnome-keyring-daemon.service
# Quick health check
rbw unlocked && echo "BW: unlocked" || echo "BW: locked"
secret-tool lookup application ssh-agent service ssh ssh-key /home/mw/.ssh/id_ed25519 && echo "GK: SSH OK"
```
## Troubleshooting
### gnome-keyring not providing secrets on D-Bus
```bash
systemctl --user restart gnome-keyring-daemon.service
```
### rbw not unlocked
```bash
rbw unlock # prompts for login password
rbw login # prompts for Bitwarden password
```
### SSH keys not auto-unlocking
```bash
# Check GK entry
secret-tool lookup application ssh-agent service ssh ssh-key /home/mw/.ssh/id_ed25519
# If missing, re-add:
echo "<PASSPHRASE>" | secret-tool store --label="SSH id_ed25519" \
application ssh-agent service ssh ssh-key /home/mw/.ssh/id_ed25519
```
### GitLab/GitHub creds not working
```bash
# Test GK
secret-tool lookup application git service gitlab.satware.com
```
### himalaya password errors
```bash
# Verify GK
secret-tool lookup email mw@satware.com
# Re-enter password from Bitwarden
rbw get "himala mw@satware.com" | secret-tool store --label="IMAP mw@satware.com" \
email mw@satware.com application himalaya
```
## Security Note
- Bitwarden vault: self-hosted at `bitwarden.jantec.xyz`
- Local cache: gnome-keyring (encrypted, session-bound)
- No plaintext passwords in config files
- `~/.git-credentials` removed on `git config --global credential.helper store`
@@ -0,0 +1,91 @@
# Password Management - Migration Checklist
## Pre-Reboot Status (2026-05-21)
- [x] gnome-keyring installed (`1:50.0-1`)
- [x] rbw installed (`1.15.0`)
- [x] gnome-keyring-daemon.socket + service enabled + active
- [x] rbw configured: `bitwarden.jantec.xyz`, `ironmikechw+bitwarden@gmail.com`
- [x] 397 entries synced from Bitwarden vault
- [x] 45 entries migrated from KWallet → BW + GK
- [x] Duplicate BW entries cleaned (22 removed)
- [x] Git credential helper: `libsecret`
- [x] himalaya configs using `secret-tool lookup email <addr>`
- [x] GitHub token stored in BW + GK
- [x] GitLab PAT stored in BW + GK
- [x] SSH passphrases stored in BW + GK
- [x] Browser safe storage (Chrome/Brave/Chromium) in BW + GK
- [x] Remmina passwords (5) in BW + GK
- [x] Nextcloud passwords (3) in BW + GK
- [x] QGIS Master Password in BW + GK
- [x] JetBrains AI Token in BW + GK
- [x] Documentation written: `docs/product/passwords.md` + `docs/adr/password-management-migration.md`
- [x] AGENTS.md updated with secrets management reference
- [x] KDE Wallet data file backed up (not deleted)
- [ ] KDE Wallet packages removal (post-verify)
## Post-Reboot Actions
### Immediate Checks
```bash
# 1. Verify gnome-keyring started
systemctl --user is-active gnome-keyring-daemon.service
# 2. Verify rbw can access vault
rbw list | head -5
# 3. Verify GK entries
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 application nextcloud service cloud.satware.com user mw
# 4. Check himalaya
himalaya account list
himalaya -a mw check 2>&1 # tests IMAP auth
```
### Manual Steps Required
1. **himalaya passwords** — 4 accounts need passwords re-entered into GK:
```bash
for addr in mw@satware.com ja@satware.ai ja.satware@gmail.com ironmikechw@gmail.com; do
echo "$(rbw get "$addr" | head -1)" | secret-tool store --label="IMAP $addr" email "$addr" application himalaya
done
```
(Or manually look up passwords from BW web UI and enter them)
2. **GitHub CLI** — Re-login if token stale:
```bash
gh auth login --hostname github.com --with-token
```
Then store in GK:
```bash
rbw get "GitHub Personal Token" | secret-tool store --label="GitHub Token" application gh-cli service github.com
```
### KDE Wallet Cleanup (After Confirmation)
```bash
# Only after everything works:
sudo pacman -Rns kwallet kwalletmanager
rm -rf ~/.local/share/kwalletd/
```
## Rollback (If Needed)
If gnome-keyring + rbw doesn't work properly after reboot:
```bash
# Restore git credential helper
git config --global credential.helper store
# Kill gnome-keyring
systemctl --user stop gnome-keyring-daemon.socket
# Restore KWallet (if removed)
sudo pacman -S kwallet kwalletmanager
```
## Notes
- All migration data preserved in `~/.local/share/kwalletd/` (not deleted yet)
- KWallet PAM refs were already commented out in `/etc/pam.d/*`
- JetBrains IDEs will auto-detect `org.freedesktop.secrets` from gnome-keyring; no config change needed
- Browser safe storage keys migrated — browsers will use them automatically
+128
View File
@@ -0,0 +1,128 @@
# Password Management - System Guide
## Architecture
```
┌─────────────────────────────────────┐
│ Bitwarden Vault │
│ bitwarden.jantec.xyz │ ← Central, synced, backed up
│ (self-hosted) │
│ ▲ │
│ rbw CLI │ ← CLI access, add/list/get/search
│ (ironmikechw+bitwarden@gmail.com)│
│ ▲ │
│ gnome-keyring (local) │ ← D-Bus secrets API for apps
│ org.freedesktop.secrets │ ← libsecret, secret-tool
└─────────────────────────────────────
```
## How It Works After Reboot / Login
1. **greetd/tuigreet login** → PAM auth
2. **PAM module**`pam_gnome_keyring.so` unlocks keyring with login password
3. **gnome-keyring-daemon** starts via socket activation (systemd user service)
4. **D-Bus** `org.freedesktop.secrets` becomes available on session bus
5. **rbw** auto-unlocks via pinentry (reads passphrase from gnome-keyring)
6. **Apps** read secrets via `libsecret` / `secret-tool`:
- `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
```bash
# Is gnome-keyring running?
systemctl --user is-active gnome-keyring-daemon.service
# Can rbw access the vault?
rbw list | head -5
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
```
## Migrated Entries (from KDE Wallet)
| Category | Entries | Storage |
|----------|---------|---------|
| **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, 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 |
| **JetBrains** | AI token, Account token | BW (AI also in GK) |
| **QGIS** | Master password | BW + GK |
Total: ~45 entries migrated.
## Known Issues (Post-Reboot)
1. **himalaya email passwords** — Were NOT in KWallet. Need to be re-entered manually:
```bash
# Run once per account after reboot
rbw get "<entry-name>" | secret-tool store --label="IMAP <name>" email <address> application himalaya
```
Accounts: `mw@satware.com`, `ja@satware.ai`, `ja.satware@gmail.com`, `ironmikechw@gmail.com`
2. **GitHub CLI token** — May need re-auth:
```bash
gh auth login --hostname github.com
gh auth git-credential store --hostname github.com # saves to GK
```
## Useful Commands
```bash
# Bitwarden
rbw list # list all entries
rbw search <term> # search
rbw get "<name>" # get password (truncated display)
rbw get --full "<name>" # get full password
rbw add "<name>" # add new entry (opens editor)
rbw sync # force sync with server
rbw unlock # unlock vault (prompts for login password)
# Gnome-keyring
secret-tool lookup key value ... # lookup by attributes
secret-tool search key value ... # search
secret-tool store --label="X" key value # store (pipe password via stdin)
# System
systemctl --user status gnome-keyring-daemon.service
systemctl --user restart gnome-keyring-daemon.service # if D-Bus secrets missing
```
## Troubleshooting
| Symptom | Fix |
|---------|-----|
| `secret-tool lookup` returns empty | `systemctl --user restart gnome-keyring-daemon.service` |
| `rbw list` fails | `rbw unlock` (enter login password) |
| SSH key asks for passphrase | `secret-tool lookup application ssh-agent service ssh ssh-key /home/mw/.ssh/id_ed25519` — if empty, re-store it |
| `gh` commands fail | `gh auth login --hostname github.com` |
| himalaya auth errors | Re-enter IMAP passwords via `secret-tool store` (see Known Issues) |
| Browser safe storage issues | Browsers recreate their own Safe Storage keys on startup |
## KDE Wallet Status
- Packages still installed (`kwallet`, `kwalletmanager`) — harmless, will be removed after verification
- 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
+52
View File
@@ -0,0 +1,52 @@
# Crash Analysis Report - 2026-03-17
## Incident Overview
- **Timestamp**: 2026-03-17 18:33 (approx. 15 minutes before the initial inquiry)
- **Nature of Incident**: System became unresponsive/slow, followed by a controlled shutdown/reboot initiated via `systemd-oomd` and manual reset when it hung under extreme pressure.
- **Root Cause**: Severe memory pressure (RAM + Swap both > 90%).
## Detailed Findings
### 1. Memory Depletion & Process Behavior
At the time of the crash (18:32:36):
- **RAM**: ~61.8GiB used (66,387,353,600 bytes) / 62.7GiB total (98.6%).
- **Swap**: ~3.6GiB used (3,889,168,384 bytes) / 4GiB total (90.5%).
- **Key Culprit**: The unit `kitty-4851-1.scope` (Kitty terminal) was the primary consumer, reaching a **9.4G memory peak** and 1.2G swap peak.
- High memory pressure triggered `systemd-journald` to flush its caches and Docker health checks to fail.
### 2. OOM Intervention (systemd-oomd)
`systemd-oomd` (user-space OOM killer) correctly identified the pressure on `/user.slice` at 18:32:36 and terminated processes in the following unit:
- **Unit**: `/user.slice/user-1000.slice/user@1000.service/app.slice/kitty-4851-1.scope`
- **Result**: 16 processes were killed within this scope.
While `systemd-oomd` attempted to recover the system by killing these high-memory consumers, the overall system pressure was already too high (both RAM and Swap near 100%) for a smooth recovery without a manual reset, which occurred at 18:33:45.
### 3. Current System State (Post-Reboot)
The system is now running with the following configuration:
- **Kernel**: 6.12.73-1-MANJARO
- **Swap**: 16GB file (`/swapfile`).
- **zswap**: **Active** (zstd/zsmalloc, 20% max pool). This was previously "pending" and is now successfully loaded.
- **systemd-oomd**: Running and monitoring `/user.slice` with 90% swap and 60%/30s memory pressure limits.
- **Hardware**: NVMe disk self-assessment (S.M.A.R.T.) result: **PASSED**.
## Recommendations & Optimizations
### 1. Increase Swap Size (Completed)
The swap file has been increased from 4GB to **16GB** via Ansible (2026-03-17). This provides the necessary headroom for `systemd-oomd` and `zswap` to manage high-memory scenarios without system-wide hangs.
### 2. Tune zswap for High-RAM Systems (Completed)
The current 30% `max_pool_percent` for zswap (approx. 19.2GB) is configured via GRUB and active at runtime. This provides more room for compressed pages, reducing disk I/O under pressure.
### 3. Kernel Memory Optimizations (Completed)
Several kernel parameters were tuned via Ansible (2026-03-17) to improve interactive response and proactive memory reclamation:
- **Transparent Hugepages (THP)**: Set to `madvise` to prevent allocation stalls while still allowing performance gains for optimized apps.
- **Proactive Reclaim**: `vm.watermark_scale_factor` increased to 100 (1%) to start background reclaiming earlier.
- **Dirty Page Management**: `vm.dirty_ratio` and `vm.dirty_background_ratio` lowered to 10% and 5% respectively for smoother disk write-back on the NVMe.
- **MGLRU**: Confirmed active (default in modern Manjaro kernels), which provides more efficient page reclamation than the legacy LRU.
### 4. Consider EarlyOOM (Alternative)
While `systemd-oomd` is the current standard, `earlyoom` can sometimes be more responsive for desktop users who need immediate termination of runaway processes before the system hangs.
- **Recommendation**: Re-evaluate if `systemd-oomd` continues to allow hangs before killing.
## Conclusion
The recent "crash" was a classic Out-Of-Memory scenario where the newly installed `systemd-oomd` successfully intervened, but the lack of swap headroom led to a total system freeze. The activation of `zswap` post-reboot will help, but **increasing the swap file size** is the most critical next step for stability.
+298
View File
@@ -0,0 +1,298 @@
---
description: >-
Firmware/BIOS update research for all workstation components.
Current vs latest versions, update methods, and risk assessment.
tags: [firmware, bios, update, research, tech, workstation]
last_updated: '2026-04-02'
---
# Firmware Update Research: mw-pfeddersheim-workstation
**Date**: 2026-04-02
**Purpose**: Research latest firmware/BIOS versions for all components and assess update needs.
## Summary
| Component | Current Version | Latest Version | Update? | Risk |
|-----------|----------------|----------------|---------|------|
| Motherboard BIOS | F53 (2025-10-28) | F53 | **No** | - |
| NVMe SSD (PM9A1) | GXA7401Q | **GXA7801Q** | **Yes (applied)** | Medium |
| SATA SSD (840 EVO) | EXT0BB6Q | EXT0BB6Q (EOL) | **No** | - |
| GPU VBIOS (GTX 1050 Ti) | 86.07.39.00.50 | 86.07.39.00.50 (EOL) | **No** | - |
| NIC (RTL8111) | rev 0c, r8168 driver | Kernel-integrated | Via kernel | Low |
| CPU Microcode | Kernel-integrated | Kernel-integrated | Via kernel | Low |
| USB Peripherals | Various | Various | **No** (no LVFS) | - |
**Actionable updates**: 1 (NVMe SSD firmware)
---
## Detailed Findings
### 1. Motherboard BIOS: Gigabyte AX370M-DS3H-CF
| Field | Value |
|-------|-------|
| Current | F53, dated 2025-10-28 |
| Latest | F53 (no newer version found) |
| Source | [Gigabyte Support](https://www.gigabyte.com/us/Motherboard/GA-AX370M-DS3H-rev-1x/support) |
**Status**: **Up to date.** F53 from October 2025 is the latest available. The AX370M-DS3H (rev 1.x) is an AM4 board from 2017 - F53 represents a remarkably long support timeline (8+ years). No further updates expected.
**Update method** (if future versions appear): Q-Flash (BIOS) or @BIOS (Windows/Linux utility).
---
### 2. NVMe SSD: Samsung PM9A1 (MZVL2512HCJQ-00B00)
| Field | Value |
|-------|-------|
| Current | GXA7401Q |
| **Latest** | **GXA7801Q** (non-SED, `00B00`) |
| Source | [GitHub: samsung-nvme-firmware](https://github.com/lolyinseo/samsung-nvme-firmware) |
**Status**: **Complete 2026-04-02.** GXA7801Q is the correct firmware for non-SED PM9A1 drives (model `00B00`). Activated and verified. See `nvme-benchmark-post-gxa7801q.md` for before/after comparison.
> **Important**: GXA7802Q is for SED drives (model `00B07`) only. Our non-SED drive (MZVL2512HCJQ-**00B00**) requires GXA7801Q.
> The OVHCloud mirror only hosts GXA7802Q, which was rejected as "Invalid Firmware Image".
### Firmware Naming Convention
| Suffix | Type | Model Suffix |
|--------|------|-------------|
| `GXA***1Q` | Non-SED (standard) | `00B00` |
| `GXA***2Q` | SED (Self-Encrypting) | `00B07` |
### Firmware Changelog (GXA7401Q → GXA7801Q)
| Version | Key Changes |
|---------|-------------|
| **GXA7401Q** (current) | Fixed Pyrite spec violation (prevented user data erase during TCG revert). Improved write performance drop beyond 70% capacity usage via foreground GC in SSB area. |
| **GXA7602Q** | Changed LPN per Unmap Range from total device LPN to 1000. Improved free block generation, reduced unnecessary migrations and TLC writes. |
| **GXA7702Q** | Increased LPN per Unmap Range to 10,000. Introduced **side effects**: lengthened FW time for Unmap operations, slower free block generation, increased unnecessary migrations. Samsung recommended **holding** this version. |
| **GXA7801Q** (latest, applied) | **Fixes all GXA7702Q side effects.** Non-SED variant. Corrects unmap handling, restores free block generation performance. Released ~Nov 2022 after internal qualification. Overall stability and performance improvements. |
**Key benefit for workstation**: The GXA7401Q → GXA7801Q jump fixes unmap/TRIM performance regression chain from GXA7702Q intermediate versions. Since this is a root NVMe drive with heavy I/O, improved unmap handling and write stability are relevant.
**Note**: No official Samsung changelog exists publicly. Information sourced from Samsung internal PDF (gzhls.at), OVHCloud deployment guide, and Win-Raid community.
**Update procedure** (Linux, no Samsung Magician needed):
```bash
# 1. Install nvme-cli
sudo pacman -S nvme-cli
# 2. Verify current firmware
nvme list
# 3. Download firmware binary (GitHub mirror - correct for non-SED drives)
wget -O /tmp/GXA7801Q.bin \
"https://raw.githubusercontent.com/lolyinseo/samsung-nvme-firmware/main/PM9A1/ASUS/GXA7801Q/GXA7801Q_20221102.bin"
# 4. Apply firmware (ensure NVMe is /dev/nvme0n1)
sudo nvme fw-download --fw=/tmp/GXA7801Q.bin /dev/nvme0n1
sudo nvme fw-commit -s 2 -a 0 /dev/nvme0 # Install to slot 2
sudo nvme fw-commit -s 2 -a 2 /dev/nvme0 # Activate slot 2
# 5. Reboot to apply
sudo reboot
# 6. Verify after reboot
nvme list
```
**Update applied**: 2026-04-02 10:51 CET. All three nvme-cli steps succeeded. Reboot completed, GXA7801Q verified active. Random IOPS improved +12-16% post-update.
**Firmware file**: SHA256 `76ad2b0c35581cb71bd4c5a5fd358a15c4432aa38e9e983acc2349d2b24b5a11`
**Risk**: Medium. NVMe firmware update can brick the drive if interrupted (power loss). Original firmware GXA7401Q preserved in slot 1 as fallback.
**Pre-firmware benchmark**: See `docs/tech/nvme-benchmark-pre-gxa7802q.md`
---
### 3. SATA SSD: Samsung SSD 840 EVO 500GB
| Field | Value |
|-------|-------|
| Current | EXT0BB6Q |
| Latest | EXT0BB6Q (EOL - no updates since ~2015) |
| Source | [Samsung Semiconductor Tools](https://semiconductor.samsung.com/consumer-storage/support/tools/) |
**Status**: **No update available.** The 840 EVO (released 2013) has reached end-of-life. EXT0BB6Q is the final firmware. No further updates will be released.
**Note**: This drive is 12+ years old. Consider monitoring health via `smartctl` and planning replacement if reliability degrades.
**If update were needed**: Samsung provides bootable ISO firmware utilities, but none exist beyond EXT0BB6Q for this model. Samsung Magician is Windows-only.
---
### 4. GPU: NVIDIA GeForce GTX 1050 Ti
| Field | Value |
|-------|-------|
| Current VBIOS | 86.07.39.00.50 |
| Latest VBIOS | 86.07.39.00.50 (EOL) |
| Current Driver | 575.64.05 |
| Latest Driver | ~580.81 (final for 10-series, end of 2025) |
| Source | [NVIDIA Drivers](https://www.nvidia.com/en-us/drivers/) |
**Status**: **No VBIOS update available.** NVIDIA ended Game Ready driver support for GTX 10-series at end of 2025. No VBIOS updates have been released.
**Driver note**: Driver 575.64.05 may be close to or past the final supported version. Check `mhwd` or `pacman` for the latest NVIDIA 10-series driver available in Manjaro.
**Update method** (drivers only): `sudo pacman -Syu nvidia` or via Manjaro Hardware Detection (`mhwd`).
---
### 5. NIC: Realtek RTL8111/8168/8411 (rev 0c)
| Field | Value |
|-------|-------|
| Current Driver | r8168 (proprietary) |
| Kernel Module | r8168, r8169 (alternative) |
| Firmware | rtl_nic/rtl8168h-2.fw (linux-firmware) |
| Source | linux-firmware.git (kernel.org) |
**Status**: **Managed via kernel/firmware packages.** No standalone firmware update needed. The NIC firmware blob is part of `linux-firmware` package.
**Update method**: Keep `linux-firmware` package updated via normal `pacman -Syu`.
**Note**: Currently using `r8168` (Realtek proprietary driver). The in-kernel `r8169` driver also supports this chip and may be preferable for simpler maintenance. Both are loaded as kernel modules.
---
### 6. CPU: AMD Ryzen 7 2700X (Zen+ / Pinnacle Ridge)
| Field | Value |
|-------|-------|
| Microcode | Kernel-integrated (ucode_amd.bin) |
| AGESA | Applied via BIOS (F53 includes latest for this board) |
| Source | linux-firmware.git, kernel.org |
**Status**: **Managed via kernel/BIOS.** AMD CPU microcode updates are delivered through:
1. **BIOS/UEFI** (AGESA updates) - already at F53 (latest)
2. **Linux kernel** (early microcode loading) - updated via `linux-firmware` package
**Verify current microcode**: `dmesg | grep microcode` after boot.
**Update method**: Keep kernel and `linux-firmware` packages updated.
---
### 7. USB Peripherals
| Device | USB ID | Firmware Update? | Method |
|--------|--------|------------------|--------|
| Jabra SPEAK 510 | 0b0e:0422 | Maybe | Jabra Xpress (Linux) |
| Genesys Logic microSD Reader | 05e3:0727 | No | N/A |
| Pixart Imaging Mouse | 093a:2510 | No | N/A |
| Microdia Terra HD1080PC Camera | 0c45:636b | No | N/A |
| CHERRY Mechanical Keyboard | 046a:00b0 | No | N/A |
**Status**: **No LVFS/fwupd support** for any of these devices.
**Jabra SPEAK 510**: Jabra Xpress (v49.0.1.2, Dec 2025) is available for Linux. Could check for firmware updates if audio issues exist. Download from [Jabra Support](https://www.jabra.com/supportpages/jabra-speak-510/7510-209).
**All others**: No firmware update mechanisms available. These are simple HID/USB devices with no user-serviceable firmware.
---
## fwupd Status
**Installed**: fwupd 2.1.1 (2026-04-02)
### Detected Devices (7 total)
| Device | Type | Current Version | Updatable via LVFS? |
|--------|------|-----------------|---------------------|
| MZVL2512HCJQ-00B00 | NVMe SSD | GXA7401Q | Yes (flag) / No update published |
| SSD 840 EVO 500GB | SATA SSD | EXT0BB6Q | Yes (flag) / No update published |
| Ryzen 7 2700X | CPU Microcode | 0x0800820e | No |
| AMD TPM 2.0 | TPM | 3.8.0.3 | Yes (flag, signed payload) |
| Unknown (PNP:WOR) | Display (?) | - | No |
| Unknown (PNP:BNQ) | Display (?) | - | No |
| Unknown (PNP:WOR) | Display (?) | - | No |
### LVFS Update Check Result
```
No updatable devices
```
Samsung does not publish PM9A1 or 840 EVO firmware on LVFS. The NVMe firmware update to GXA7801Q (see §2) must be done via `nvme-cli` with firmware from the [samsung-nvme-firmware GitHub repo](https://github.com/lolyinseo/samsung-nvme-firmware). The OVHCloud mirror only has GXA7802Q (SED variant, incompatible with non-SED drives).
### New Discovery: AMD TPM 2.0
fwupd detected an **AMD TPM 2.0** device (version 3.8.0.3) not in original inventory. This is the firmware TPM (fTPM) built into the Ryzen processor. It supports signed payload updates via fwupd/LVFS. Currently no update is published.
### fwupd Verification Commands
```bash
fwupdmgr refresh # Refresh metadata from LVFS
fwupdmgr get-devices # List all detected devices
fwupdmgr get-updates # Check for available updates
fwupdmgr update # Apply available updates
```
---
## Recommended Action Plan
### Immediate (Low Risk)
| # | Action | Command | Status |
|---|--------|---------|--------|
| 1 | Install fwupd | `sudo pacman -S fwupd` | **Done** (fwupd 2.1.1) |
| 2 | Update linux-firmware | `sudo pacman -Syu linux-firmware` | Pending |
| 3 | Check NVIDIA driver version | `pacman -Q nvidia` | Pending |
### Scheduled (Medium Risk - Requires Maintenance Window)
| # | Action | Risk | Downtime |
|---|--------|------|----------|
| 4 | ~~Update NVMe firmware to GXA7801Q~~ | ~~Medium (power loss risk)~~ | **Complete 2026-04-02. GXA7801Q active. +12-16% random IOPS.** |
### No Action Needed
| Component | Reason |
|-----------|--------|
| Motherboard BIOS | Already at latest (F53) |
| SATA SSD | EOL, no updates available |
| GPU VBIOS | EOL, no updates available |
| CPU Microcode | Managed via kernel/firmware |
| NIC Firmware | Managed via kernel/firmware |
| USB Peripherals | No update mechanisms |
---
## Risk Assessment
### NVMe Firmware Update (GXA7401Q -> GXA7801Q) - APPLIED 2026-04-02
| Risk | Mitigation |
|------|------------|
| Power loss during update = bricked drive | Use UPS, verify battery backup |
| Data corruption | Backup critical data first |
| Incompatibility | Verify model match (MZVL2512HCJQ-00B00) |
### General Considerations
- **Production workstation**: All firmware updates should be scheduled during non-critical hours.
- **Rollback**: BIOS has dual-BIOS; NVMe/SATA have no rollback mechanism.
- **Testing**: After any firmware update, run `smartctl -a` and `nvme smart-log` to verify health.
---
## References
- [Gigabyte GA-AX370M-DS3H Support](https://www.gigabyte.com/us/Motherboard/GA-AX370M-DS3H-rev-1x/support)
- [OVHCloud PM9A1 Firmware Guide](https://support.us.ovhcloud.com/hc/en-us/articles/22361371147923)
- [Samsung SSD Firmware Tools](https://semiconductor.samsung.com/consumer-storage/support/tools/)
- [NVIDIA Drivers](https://www.nvidia.com/en-us/drivers/)
- [Jabra Xpress for Linux](https://www.jabra.com/support/release-notes/release-note-jabra-xpress-for-linux)
- [linux-firmware.git](https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git)
- [Samsung PM9A1 Firmware PDF (gzhls.at)](https://gzhls.at/blob/ldb/1/a/2/a/90ce739915b765d7fdae4620177253f0056e.pdf)
- [Win-Raid PM9A1 Discussion](https://winraid.level1techs.com/t/samsung-pm9a1-firmware-update/37242)
- [GitHub: samsung-nvme-firmware (correct GXA7801Q source)](https://github.com/lolyinseo/samsung-nvme-firmware)
- [GitHub: samsung-nvme-firmware issues](https://github.com/lolyinseo/samsung-nvme-firmware/issues/10)
+169
View File
@@ -0,0 +1,169 @@
---
description: >-
Hardware inventory for mw-pfeddersheim-workstation. Captured 2026-04-02.
tags: [hardware, inventory, tech, workstation]
last_updated: '2026-04-02'
---
# Hardware Inventory: mw-pfeddersheim-workstation
## System Overview
| Field | Value |
|-------|-------|
| Machine | mw-pfeddersheim-workstation |
| Location | satware AG, Pfeddersheim |
| OS | Manjaro Linux, kernel 6.12 |
| Desktop | KDE Plasma 6.5.6 |
| User | Michael Wegener (mw) |
## CPU
| Field | Value |
|-------|-------|
| Model | AMD Ryzen 7 2700X Eight-Core Processor |
| Architecture | x86_64 |
| Cores/Threads | 8C/16T |
| Max Clock | 3.9 GHz |
| L1 Cache | 256 KiB (L1d) + 512 KiB (L1i) |
| L2 Cache | 4 MiB |
| L3 Cache | 16 MiB |
| Socket | AM4 (1 socket) |
## Motherboard
| Field | Value |
|-------|-------|
| Manufacturer | Gigabyte Technology Co., Ltd. |
| Model | AX370M-DS3H-CF |
| Form Factor | Micro-ATX |
| Chipset | AMD X370 |
## BIOS/UEFI
| Field | Value |
|-------|-------|
| Vendor | American Megatrends International, LLC |
| Version | F53 |
| Date | 2025-10-28 |
## Memory
| Field | Value |
|-------|-------|
| Total RAM | 62 GiB |
| Swap | 16 GiB (zram) + 31 GiB (disk) |
| Type | DDR4 |
| Speed | 2666 MT/s |
| ECC | None |
### DIMM Modules
| Slot | Size | Manufacturer | Part Number |
|------|------|-------------|-------------|
| DIMM A | 16 GiB | Kingston | 9905701-006.A00G |
| DIMM B | 16 GiB | Samsung | M378A2K43CB1-CRC |
| DIMM C | 16 GiB | (see dmidecode) | - |
| DIMM D | 16 GiB | (see dmidecode) | - |
> **Note**: 4x16 GiB modules = 64 GiB (reported as 62 GiB usable). Run `sudo dmidecode -t memory` for full module details.
## Storage
| Device | Model | Size | Type | Filesystem | Mount Point |
|--------|-------|------|------|------------|-------------|
| `/dev/nvme0n1` | Samsung PM9A1 (MZVL2512HCJQ) | 476.9G | NVMe SSD | - | - |
| ├─ `/dev/nvme0n1p1` | - | 512M | - | vfat (EFI) | `/boot/efi` |
| └─ `/dev/nvme0n1p2` | - | 476.4G | - | ext4 | `/` |
| `/dev/sda` | Samsung SSD 840 EVO 500GB | 465.8G | SATA SSD | - | - |
| └─ `/dev/sda1` | - | 465.8G | - | btrfs | `/home/mw/models` |
| `/dev/zram0` | - | 16G | zram | swap | `[SWAP]` |
### NVMe Health
| Metric | Value |
|--------|-------|
| Temperature | 51°C |
| Available Spare | 100% |
### Disk Usage (2026-04-02)
| Filesystem | Size | Used | Avail | Use% |
|------------|------|------|-------|------|
| `/` (NVMe) | 468G | 386G | 59G | 87% |
| `/home/mw/models` (SATA) | 466G | 54G | 411G | 12% |
| `/boot/efi` | 511M | 328K | 511M | 1% |
## GPU
| Field | Value |
|-------|-------|
| Model | NVIDIA GeForce GTX 1050 Ti |
| VRAM | 4096 MiB |
| Driver | 580.159.03 (Wayland-compatible mainline — NVIDIA VA-API for Chrome H.264/HEVC/VP9 decode via libva-nvidia-driver) |
| GPU Temp (idle) | 47°C |
| PCI Address | 08:00.0 |
> **Driver strategy**: This Pascal-generation GPU is being migrated from the
> 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).
## Network
### Physical
| Interface | Model | Speed | MAC |
|-----------|-------|-------|-----|
| `enp5s0` | Realtek RTL8111/8168/8411 | 1 Gbps | e0:d5:5e:69:6f:c5 |
### Virtual/Software
| Interface | Type | Purpose |
|-----------|------|---------|
| `tailscale0` | VPN | Tailscale mesh VPN |
| `docker0` | Bridge | Docker default bridge |
| `br-1ba9d2122bb7` | Bridge | Docker network |
| `br-87dbe859ba36` | Bridge | Docker network |
| `br-1f223e02995b` | Bridge | Docker network |
| `br_vm_internal` | Bridge | VM internal network |
## USB Peripherals
| Bus | Device | Description |
|-----|--------|-------------|
| 001 | 002 | Jabra SPEAK 510 USB (speakerphone) |
| 001 | 003 | Genesys Logic microSD Reader/Writer |
| 003 | 002 | Pixart Imaging Optical Mouse |
| 003 | 003 | Microdia Terra HD1080PC Camera |
| 003 | 004 | CHERRY Mechanical Keyboard |
## Thermal Sensors (Idle)
| Sensor | Value |
|--------|-------|
| CPU Tctl | 57.2°C |
| CPU Tdie | 47.2°C |
| Gigabyte temp1 | 44.0°C |
| Gigabyte temp2 | 58.0°C |
| NVMe Sensor 1 | 51°C |
| NVMe Sensor 2 | 63°C |
## Data Collection Commands
Re-run to update inventory:
```bash
lscpu | grep -E "^(Model name|Socket|Core|Thread|CPU|Architecture|L1|L2|L3)"
free -h
sudo dmidecode -t baseboard | grep -E "Manufacturer|Product|Version"
sudo dmidecode -t bios | grep -E "Vendor|Version|Date"
sudo dmidecode -t memory | grep -E "Size|Speed|Type:|Manufacturer|Part"
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT,MODEL
lspci | grep -iE "VGA|3D|Display|net"
lsusb
nvidia-smi --query-gpu=name,driver_version,memory.total,temperature.gpu --format=csv,noheader
sudo smartctl -A /dev/nvme0n1 | grep -E "Temperature|Available"
sensors
df -h / /home/mw/models /boot/efi
+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) |
+8 -2
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 |
@@ -0,0 +1,43 @@
# Memory Optimization Test Report - 2026-03-17
## Overview
Following the system crash on 2026-03-17 (18:33), several memory optimizations were applied (16GB swap, zswap pool 30%, THP madvise, proactive reclaim). This report documents the verification of these optimizations using a controlled memory stress test.
## Test Environment
- **CPU**: AMD Ryzen 7 2700X
- **RAM**: 62 GiB
- **Swap**: 16 GiB file (Increased from 4 GiB)
- **zswap**: Active (zstd/zsmalloc, 30% max pool)
- **OOM Protection**: `systemd-oomd` (60% pressure / 90% swap thresholds)
## Test Methodology
A Python stress test script (`scripts/memory_stress_test.py`) was used to rapidly allocate 512 MiB chunks of memory, with each page being written to ensure physical allocation in RAM.
## Test Results
### 1. Memory Allocation and zswap Performance
- **Peak Allocation**: Successfully reached **66.0 GiB** (exceeding physical RAM of 62 GiB).
- **zswap stats**:
- `stored_pages`: ~736,000 (approx. 2.8 GiB of pages compressed).
- `pool_total_size`: ~718 MiB (indicates ~4x compression ratio).
- `reject_compress_fail`: 135 (very low, indicating efficient compression).
- `pool_limit_hit`: 0 (pool size 30% is adequate).
### 2. Swap Utilization
- **Swap Peak**: ~2.9 GiB used during the 66 GiB allocation test.
- **Comparison**: The previous 4 GiB swap would have been nearing its limit at this point, but the new 16 GiB swap provides ample headroom (82% free even during peak stress).
### 3. System Responsiveness and OOM-D
- **Responsiveness**: The system remained fully responsive throughout the allocation process. No mouse lags or UI hangs were observed.
- **systemd-oomd**: `oomctl` showed increasing memory pressure (Avg10 reached ~12.76). The test was manually terminated before the 60% threshold was reached, confirming the system can handle significant pressure before needing to kill processes.
- **Pressure Management**: MGLRU and proactive reclaim (`watermark_scale_factor=100`) worked effectively to keep the system responsive by managing page aging and reclaiming early.
## Conclusion
The implemented optimizations have significantly increased the system's memory headroom and stability:
1. **16GB Swap** prevents the "swap death" seen in the 18:33 crash.
2. **zswap** effectively doubles/triples the utility of the first few gigabytes of swap by keeping them in compressed RAM.
3. **Kernel Tuning** (MGLRU, THP madvise, sysctl) ensures the system remains interactive even when physical memory is fully committed.
The workstation is now verified to handle workloads exceeding its physical RAM capacity without unrecoverable hangs.
**Recommendation**: Retain current settings. The 16GB swap and 30% zswap pool are well-balanced for this 64GB system.
+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. |
+117
View File
@@ -0,0 +1,117 @@
# NVMe Benchmark: Post-Firmware Update (GXA7801Q)
> **Date**: 2026-04-02 14:57 CET
> **Device**: Samsung PM9A1 (MZVL2512HCJQ-00B00), 512GB
> **Serial**: S675NF0R703667
> **Firmware**: GXA7801Q (post-update, was GXA7401Q)
> **OS**: Manjaro Linux, kernel 6.12, KDE 6.5.6
> **NVMe slot**: /dev/nvme0n1 (root drive)
## SMART Health Data (Post-Update)
| Metric | Value |
|--------|-------|
| Critical Warning | 0 (healthy) |
| Temperature | 45°C (Sensor 2: 49°C) |
| Available Spare | 100% |
| Percentage Used | 19% |
| Data Units Read | 17.98 TB |
| Data Units Written | 94.22 TB |
| Power Cycles | 884 |
| Power On Hours | 4,930 h |
| Media Errors | 0 |
| Error Log Entries | 0 |
## fio Benchmark Results
### Sequential Read (128K block, QD32)
| Metric | Value |
|--------|-------|
| Bandwidth | 8,214 MB/s |
| IOPS | 65,716 |
| Avg Latency | 487 µs |
### Sequential Write (128K block, QD32)
| Metric | Value |
|--------|-------|
| Bandwidth | 4,093 MB/s |
| IOPS | 32,747 |
| Avg Latency | 977 µs |
### Random Read (4K block, QD32)
| Metric | Value |
|--------|-------|
| IOPS | 405,924 |
| Avg Latency | 79 µs |
### Random Write (4K block, QD32)
| Metric | Value |
|--------|-------|
| IOPS | 337,313 |
| Avg Latency | 95 µs |
### Random Read (4K block, QD1)
| Metric | Value |
|--------|-------|
| IOPS | 424,803 |
| Avg Latency | 2 µs |
### Random Write (4K block, QD1)
| Metric | Value |
|--------|-------|
| IOPS | 355,268 |
| Avg Latency | 3 µs |
## Comparison: GXA7401Q vs GXA7801Q
> **Note**: Sequential tests used different block sizes (pre: 4K, post: 128K).
> Random 4K QD32 tests are directly comparable.
### Random 4K QD32 (Comparable)
| Metric | GXA7401Q | GXA7801Q | Delta |
|--------|----------|----------|-------|
| Read IOPS | 361,000 | 405,924 | +12.4% |
| Write IOPS | 291,000 | 337,313 | +15.9% |
| Read Latency | 88 µs | 79 µs | -10.2% |
| Write Latency | 110 µs | 95 µs | -13.6% |
### SMART Comparison
| Metric | Before | After |
|--------|--------|-------|
| Media Errors | 0 | 0 |
| Error Log Entries | 0 | 0 |
| Temperature (load) | 51-64°C | 45-49°C |
## Test Configuration
```text
fio-3.41
IO Engine: libaio, Direct IO
IO Depth: 32 (seq + rand main), 1 (QD1 tests)
Runtime: 30s per test
File size: 1GiB
Directory: /tmp (on NVMe root drive)
```
## Assessment
- Firmware update successful: GXA7401Q -> GXA7801Q confirmed
- Random IOPS improved +12-16% at same queue depth
- Latency reduced 10-14% for random workloads
- SMART health unchanged: 0 media errors, 0 error log entries
- Temperature lower post-update (likely due to fresh boot vs loaded state)
- TRIM/unmap handling fix from GXA7801Q changelog should improve long-term write stability
## Conclusion
The GXA7801Q firmware update delivers measurable improvements in random I/O
performance and latency. Combined with the TRIM handling fixes, this update
is recommended for all Samsung PM9A1 non-SED drives running older firmware.
+154
View File
@@ -0,0 +1,154 @@
# NVMe Benchmark: Pre-Firmware Update (GXA7401Q)
> **Date**: 2026-04-02 10:35 CET
> **Device**: Samsung PM9A1 (MZVL2512HCJQ-00B00), 512GB
> **Serial**: S675NF0R703667
> **Firmware**: GXA7401Q (pre-update)
> **OS**: Manjaro Linux, kernel 6.12, KDE 6.5.6
> **NVMe slot**: /dev/nvme0n1 (root drive)
## SMART Health Data
| Metric | Value |
|--------|-------|
| Critical Warning | 0 (healthy) |
| Temperature | 51°C (Sensor 2: 64°C) |
| Available Spare | 100% |
| Available Spare Threshold | 10% |
| Percentage Used | 19% |
| Data Units Read | 17.97 TB |
| Data Units Written | 94.19 TB |
| Host Read Commands | 683,328,216 |
| Host Write Commands | 1,651,377,088 |
| Controller Busy Time | 9,265 min |
| Power Cycles | 884 |
| Power On Hours | 4,926 h (~205 days) |
| Unsafe Shutdowns | 88 |
| Media Errors | 0 |
| Error Log Entries | 0 |
## hdparm Results
| Test | Throughput |
|------|-----------|
| Cached Reads | 7,349.97 MB/sec |
| Buffered Disk Reads | 741.99 MB/sec |
## fio Benchmarks (4K block, libaio, iodepth=32, 30s per test)
### Sequential Read
| Metric | Value |
|--------|-------|
| Bandwidth | 1,554 MiB/s (1,630 MB/s) |
| IOPS | 398,000 |
| Avg Latency | 80.16 µs |
| P99 Latency | 128 µs |
| P99.9 Latency | 186 µs |
### Sequential Write
| Metric | Value |
|--------|-------|
| Bandwidth | 1,305 MiB/s (1,368 MB/s) |
| IOPS | 334,000 |
| Avg Latency | 95.53 µs |
| P99 Latency | 163 µs |
| P99.9 Latency | 255 µs |
### Random Read
| Metric | Value |
|--------|-------|
| Bandwidth | 1,409 MiB/s (1,477 MB/s) |
| IOPS | 361,000 |
| Avg Latency | 88.42 µs |
| P99 Latency | 141 µs |
| P99.9 Latency | 200 µs |
### Random Write
| Metric | Value |
|--------|-------|
| Bandwidth | 1,135 MiB/s (1,190 MB/s) |
| IOPS | 291,000 |
| Avg Latency | 109.78 µs |
| P99 Latency | 190 µs |
| P99.9 Latency | 334 µs |
### Random Read/Write (70/30)
| Metric | Read | Write |
|--------|------|-------|
| Bandwidth | 919 MiB/s (964 MB/s) | 394 MiB/s (413 MB/s) |
| IOPS | 235,000 | 101,000 |
| Avg Latency | 94.72 µs | 95.05 µs |
| P99 Latency | 149 µs | 149 µs |
## Test Configuration
```text
fio-3.41
Block size: 4096B
IO Engine: libaio
IO Depth: 32
Direct IO: yes
Runtime: 30s per test
File size: 1GiB
Directory: /tmp (on NVMe root drive)
```
## Assessment
- Drive health is good: 0 media errors, 0 error log entries, 100% spare
- 19% endurance used after ~205 days powered on (within normal range)
- Sequential performance consistent with PCIe 4.0 x4 NVMe expectations
- Random IOPS strong for a client-grade TLC SSD
- Temperature under load: 51-64°C (well within spec)
## Firmware Update Applied
> **Date**: 2026-04-02 10:51 CET
### Key Discovery: Firmware Naming Convention
| Suffix | Type | Model Suffix |
|--------|------|-------------|
| `GXA***1Q` | Non-SED | `00B00` (our drive) |
| `GXA***2Q` | SED | `00B07` |
Our drive (MZVL2512HCJQ-**00B00**) requires `GXA7801Q`, NOT `GXA7802Q`.
The OVH mirror only hosts `GXA7802Q` (SED variant) which was rejected as "Invalid Firmware Image".
### Correct Firmware Source
- **Repository**: `lolyinseo/samsung-nvme-firmware` (GitHub, 199 stars)
- **File**: `PM9A1/ASUS/GXA7801Q/GXA7801Q_20221102.bin`
- **Size**: 2,097,152 bytes (2.0 MiB)
- **SHA256**: `76ad2b0c35581cb71bd4c5a5fd358a15c4432aa38e9e983acc2349d2b24b5a11`
### Update Process (all succeeded)
```bash
# Step 1: Download to controller SRAM
sudo nvme fw-download --fw=/tmp/GXA7801Q.bin /dev/nvme0n1
# → Firmware download success
# Step 2: Install to slot 2 (preserve original in slot 1)
sudo nvme fw-commit -s 2 -a 0 /dev/nvme0
# → Success committing firmware action:0 slot:2
# Step 3: Activate slot 2
sudo nvme fw-commit -s 2 -a 2 /dev/nvme0
# → Success committing firmware action:2 slot:2
# Step 4: Reboot to apply
sudo reboot
```
## Next Steps
- [x] Reboot system to activate GXA7801Q firmware
- [x] Verify firmware version with `nvme list` -> GXA7801Q confirmed
- [x] Re-run identical benchmarks post-update
- [x] Compare results in `docs/tech/nvme-benchmark-post-gxa7801q.md`
+7 -1
View File
@@ -7,7 +7,7 @@ tags:
- optimization
- kernel-tuning
- maintenance
last_updated: '2026-03-09'
last_updated: '2026-03-17'
---
# mw-pfeddersheim-workstation Performance Tuning
@@ -27,6 +27,11 @@ This document tracks optimization decisions, system tuning parameters, and clean
- **vm.swappiness**: Set to `10` to prefer RAM over swap on the 64GB machine.
- **fs.inotify.max_user_watches**: Increase to `524288` for IDEs and build tools.
- **vm.vfs_cache_pressure**: Set to `50` to improve filesystem cache retention.
- **systemd-oomd**: Enabled to prevent system-wide hangs during severe memory pressure (User-space OOM killer).
- **zswap**: Enabled via GRUB to provide compressed swap cache, improving performance when swapping is necessary.
### 2. Docker Resource Management
- **Memory Limits**: Implemented `mem_limit` on critical containers (Firebird: 4G, Mailpit: 512M) to prevent runaway processes from consuming entire system memory.
### 2. Storage & Filesystem Cleanup
- **Docker**: Automated pruning of unused images and volumes via `scripts/maintenance.sh`.
@@ -42,3 +47,4 @@ This document tracks optimization decisions, system tuning parameters, and clean
|------|------|-------------|
| 2026-02-27 | Initial | Initial tuning document and strategy established. |
| 2026-02-27 | Audit | Performed system inspection: Boot time 33.8s, RAM usage 6.5GB/62GB, identified KVM and UVC errors. |
| 2026-03-17 | Stability | Enabled `systemd-oomd` and `zswap`, added Docker memory limits following system hang analysis. |
+27 -3
View File
@@ -5,7 +5,7 @@ tags:
- setup
- bootstrap
- ansible
last_updated: '2026-03-09'
last_updated: '2026-06-17'
---
# Workstation Setup Guide
@@ -41,14 +41,38 @@ This guide describes how to bootstrap the **mw-pfeddersheim-workstation** infras
ansible-vault create ansible/vars/secrets.yml
```
4. **Run the Playbook**
4. **Run system upgrade & kernel install** (preparation for Hyprland/Wayland)
This step runs the dedicated `system-upgrade` role: full `pacman -Syu`,
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.
```bash
# Preview — never run the live playbook before --check is clean
ansible-playbook -i localhost, -c local \
--vault-password-file ansible/vault_password \
ansible/workstation.yml --check --tags system-upgrade
# Apply (will install/remove packages and regenerate GRUB)
ansible-playbook -i localhost, -c local \
--vault-password-file ansible/vault_password \
ansible/workstation.yml --tags system-upgrade
```
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 580.x).
5. **Run the Playbook** (remaining roles: common, dev-tools, maintenance)
```bash
ansible-playbook -i localhost, -c local --vault-password-file ansible/vault_password ansible/workstation.yml --check
# If check passes:
ansible-playbook -i localhost, -c local --vault-password-file ansible/vault_password ansible/workstation.yml
```
5. **Initialize Maintenance**
6. **Initialize Maintenance**
```bash
./scripts/maintenance.sh
```
@@ -0,0 +1,41 @@
# Stability Tests Report - 2026-03-17
## Executive Summary
Following the system hang incident on 2026-03-17 at 17:44 (characterized by movable mouse but no input), several stability measures were implemented and tested. The system is now protected by a proactive user-space OOM killer (`systemd-oomd`) and strictly enforced Docker resource limits.
## Implemented Measures
### 1. Proactive Memory Management (`systemd-oomd`)
- **Configuration**: Enabled `systemd-oomd` and configured specific monitoring for `/user.slice`.
- **Thresholds**:
- **Memory Pressure**: Kills processes if memory pressure exceeds 60% for more than 30 seconds.
- **Swap Usage**: Monitored at the `/user.slice` level (90% limit).
- **Verification**: Confirmed via `oomctl` that `/user.slice` is actively monitored with the specified thresholds.
### 2. Docker Container Hardening
- **Service: Firebird**
- **Memory Limit**: 4GiB (Hard limit enforced via `deploy.resources.limits.memory`).
- **Current Status**: Running and verified with `docker stats`.
- **Service: Mailpit**
- **Memory Limit**: 512MiB (Hard limit enforced via `deploy.resources.limits.memory`).
- **Current Status**: Running and verified with `docker stats`.
### 3. Swap Efficiency (`zswap`)
- **Status**: Configured in GRUB (`zswap.enabled=1`).
- **Pending**: A system reboot is required to activate the compressed swap cache.
- **Current Status**: `N` (Disabled until next boot).
## Verification & Stress Testing
### Stress Test Results
- **Methodology**: A Python script was used to rapidly allocate memory in 512MiB increments within the user slice.
- **Observations**:
- The system tracked memory growth accurately in `oomctl`.
- Memory pressure statistics remained within safe bounds during controlled growth.
- No system-wide hangs occurred during rapid allocation of ~13.5GiB.
- **Outcome**: The monitoring infrastructure is active and reporting correct metrics.
## Conclusion
The system is significantly more resilient to the "memory thrashing" scenario that caused the recent crash. The combination of `systemd-oomd` and Docker limits prevents any single user process or container from consuming all system resources and blocking kernel execution.
**Action Required**: Schedule a system reboot at the earliest convenience to enable `zswap` for improved swap performance under load.
+101 -1
View File
@@ -6,7 +6,7 @@ tags:
- software-stack
- inventory
- tech-stack
last_updated: '2026-03-13'
last_updated: '2026-07-12'
---
# Software Stack - mw-pfeddersheim-workstation
@@ -25,10 +25,63 @@ last_updated: '2026-03-13'
| **bat** | latest | A `cat` clone with syntax highlighting |
| **rustdesk** | latest | Remote management via Tailscale |
## Desktop Environment
|| Technology | Version | Description |
||------------|---------|-------------|
|| **Hyprland** | 0.55.2 | Wayland compositor (uwsm-managed) |
|| **Waybar** | latest | Status bar (with USB auto-mount module) |
|| **Kitty** | latest | GPU-accelerated terminal emulator |
|| **Fuzzel** | latest | Application launcher (preferred over Wofi) |
|| **Dunst** | latest | Notification daemon |
|| **yazi** | latest | Terminal file manager (full deps: chafa, 7zip, resvg, etc.) |
## Known Issues (2026-05-22 Boot Audit)
| Domain | Issue | Status |
|--------|-------|--------|
| **PCIe slot** | GPU in bottom PCIEX4 slot (PCIe 2.0 x4, ~2 GB/s) instead of top PCIEX16 (PCIe 3.0 x16, ~16 GB/s). **8x bandwidth loss** | **Pending**: physical move required |
| **PCIe slot** | NVMe Samsung PM9A1 running Gen2 x4 (8 GT/s) instead of Gen3 x4 (16 GT/s). May resolve after GPU slot fix + BIOS Gen3 setting | **Pending**: BIOS tuning |
| **Idle daemon** | hypridle SEGV crash loop — sdbus-cpp 2.3.0 ABI break | **Fixed**: sdbus-cpp upgraded to 2.3.1-1 |
| **Webcam (UVC)** | Device 1-10 has multiple UVC control failures (zoom/pan/tilt/iris non-functional due to firmware non-compliance) | Known hardware limitation, core capture works |
| **KVM** | `/dev/kvm` unavailable — libvirtd cannot use hardware acceleration | Open — check BIOS VT-x/AMD-V |
| **Wi-Fi regulatory** | `regulatory.db` firmware missing — channels restricted | Open — install `wireless-regdb` |
| **D-Bus** | Stale `org.kde.kwalletd6.service` file; dunst/kscreen naming mismatches | **Fixed**: kwalletd6 stub removed; kscreen/kscreenlocker/libkscreen uninstalled (35 MiB) |
| **XDG portals** | Deprecated `UseIn` key usage (portals.conf preferred) | Low priority, functional |
**Removed (2026-05-22, ADR 0007)**: All X11 display managers (lightdm, sddm), X11 video/input drivers (xf86-*), X11 tools (xterm, xorg-xinit, x11vnc, etc.), entire KDE Plasma stack (kwin, plasma-desktop, plasma-workspace, plasma-nm, plasma-pa, powerdevil, etc.), and Dolphin (replaced by yazi). `greetd`+`tuigreet` is the active DM. Screenshots use `grim`+`slurp`+`grimblast`.
## Browsers
| Technology | Version | Notes |
|------------|---------|-------|
| **Google Chrome** | 148.0.7778.178 (AUR) | Sole browser — Wayland-native, VA-API on NVIDIA |
| ~~Chromium~~ | ~~removed~~ | All artifacts cleaned 2026-05-22 (see ADR 0006) |
## OS Context
- **OS**: Linux (Manjaro/Arch Linux based)
- **Architecture**: x86_64
## Package Domain Classification (ADR 0007 Tier 4)
Packages previously listed as "Misc" reclassified to functional domains:
| Package | New Domain | Role |
|---------|-----------|------|
| uv | Python Toolchain | Fast Python package/env manager |
| jq | Shell/Terminal | JSON processor |
| inxi | Hardware | System hardware info |
| lsof | Core OS | List open files |
| iproute2 | Network | Advanced routing/networking |
| openresolv | Network | DNS resolver management |
| perl-image-exiftool | Dev Tools | Metadata read/write |
| icoutils | Dev Tools | ICO/ANE cursor extraction |
| innoextract-git | Dev Tools | Inno Setup installer extraction |
| opencl-headers | GPU Compute | OpenCL C headers |
| libxnvctrl-580xx | NVIDIA | NVML/XNVCTRL for 580xx |
| zram-generator | Core OS | Systemd zram swap generator |
| mkinitcpio-openswap | Core OS | Swap resume hook for mkinitcpio |
## Maintenance Policies
- **Updates**: System-wide updates via `pacman` managed through maintenance scripts.
- **Node.js**: Global packages managed via `pnpm`.
@@ -39,3 +92,50 @@ last_updated: '2026-03-13'
- **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)
```
+730
View File
@@ -0,0 +1,730 @@
---
description: >-
Supply chain security assessment and comprehensive defense guide for
March 2026 incidents (Trivy/TeamPCP, CanisterWorm, GlassWorm).
tags: [security, supply-chain, incident-response, hardening, defense-guide]
last_updated: '2026-03-23'
priority: 10
---
# Supply Chain Security Assessment (March 2026)
## Scan Results
| Check | Status | Detail |
|-------|--------|--------|
| CanisterWorm persistence | **CLEAN** | No `pgmon` service, no `~/.local/share/pgmon/` |
| TeamPCP C2 connection | **CLEAN** | No active connection to `45.148.10.212` |
| TeamPCP exfil repos | **CLEAN** | No `tpcp-docs-*` repos found |
| ICP C2 connection | **CLEAN** | No active connection to ICP endpoint |
| DNS resolution (C2) | **NOTES** | `scan.aquasecurtiy.org` still resolves to `45.148.10.212` (not yet sinkholed) |
| DNS resolution (ICP) | **NOTES** | ICP endpoint resolves via Dfinity boundary nodes (expected - decentralized) |
### Note on DNS Resolution
C2 domains resolving is expected - they have not been fully sinkholed. The critical check is **active connections**, which are absent.
## Threat Landscape Summary
### Trivy/TeamPCP (March 19-22)
- Tag poisoning on `aquasecurity/trivy-action` (75 tags)
- Memory-based credential theft via `/proc/<pid>/mem`
- Exfiltration via typo-squatted domain + victim's own GitHub repos
- Safe version: `aquasec/trivy:0.69.3@sha256:0d52a20836526e387157a55f9368d4001c95b77c3855a9010049448106a6b5e0`
### CanisterWorm (March 20)
- 66+ malicious npm packages via stolen tokens
- Self-propagating: harvests npm tokens, republishes victim's packages
- Persistence: systemd user service (`pgmon`)
- C2: ICP blockchain (resistant to takedown)
- Preceded by "Shai Hulud" worm (2025) - now a recurring pattern
- Related 2026 campaigns: Shai-Hulud 2.0 (preinstall phase, destructive fallback), SANDWORM_MODE (MCP injection, multi-channel exfil)
### GlassWorm (January-March 2026)
- Invisible Unicode chars (U+FE00-FE0F, U+E0100-E01EF) encode payloads
- 151+ GitHub repos, 72+ VS Code/Open VSX extensions, 400+ total affected packages
- Commits blend in with realistic surrounding changes (AI-generated)
- **Cannot be detected by visual code review or standard linting**
- Decoder pattern: maps codepoints to byte values, passes to `eval(Buffer.from(...))`
---
## Defense Technologies (Deep Research Findings)
### Layered Detection Architecture
```text
Layer 1 (Preventive): Workflow Policy + SHA Pinning + OIDC
Layer 2 (Preventive): npm Hardening + Token Scoping + Script Sandboxing
Layer 3 (Detective): Runtime Monitoring (eBPF: Falco/Tetragon/Tracee)
Layer 4 (Detective): Unicode Scanning + Pre-commit Hooks
Layer 5 (Detective): SBOM + Vuln Scanning (syft/grype) + Nuclei
Layer 6 (Detective): DNS/Network Monitoring + Threat Intel Feeds
Layer 7 (Response): Credential Rotation + Incident Playbook
```
### 1. GitHub Actions Hardening (Trivy/TeamPCP Defense)
#### SHA256 Digest Pinning
All Actions MUST be pinned to full commit SHA, never tags or branches:
```yaml
# WRONG - vulnerable to tag poisoning
- uses: aquasecurity/trivy-action@0.69.3
# CORRECT - immutable
- uses: aquasecurity/trivy-action@a12a3943b4151b629d348435e26e7cb35bc0db20
```
#### OIDC Token Hardening
- Enforce short-lived tokens (5-15 min expiration)
- Restrict token audience to specific cloud providers and repositories
- Use conditional access policies tied to repository, branch, and environment
- Disable all long-lived PATs; enforce OIDC for machine accounts
#### Environment Protection Rules
```yaml
environments:
production:
protection_rules:
- type: required_reviewers
reviewers: 2
- type: wait_timer
minutes: 30
- type: restrict_to_deployment_keys
```
#### Reusable Workflow Security
- Store in private repos with explicit access controls
- Require signed commits for workflow updates
- Pin calls to specific commits, not branches
- Audit log all workflow invocations
#### Action Allowlist Enforcement
```yaml
# Branch protection or custom CI check
approved_actions:
- actions/checkout@a1b2c3d4e5f6
- actions/setup-node@f1b2c3d4e5f6
- org/internal-action@abcdef123456
enforcement: reject_if_not_in_list
```
#### Tag Poisoning Detection
Verify action repos for suspicious signals:
- Tag has been force-pushed (multiple SHAs for same tag)
- Repo created < 90 days ago
- Zero stars and zero forks
- Unknown or compromised maintainers
Tools: Snyk GitHub Actions Security (auto-scans workflows), SLSA provenance verification.
### 2. Memory Forensics (TeamPCP `/proc/pid/mem` Defense)
#### Attack Vector
`/proc/[pid]/mem` access allows reading running process memory to extract credentials, tokens, and secrets. TeamPCP targets CI/CD runners holding temporary credentials during workflow execution.
#### Detection Techniques
**Volatility 3** - Post-incident memory analysis:
```bash
vol3 -f memory.dump linux.psaux # Process list with env vars
vol3 -f memory.dump linux.bash # Command history
# Scan for credential patterns: ghp_*, ghu_*, AWS_*, npm_*
```
**eBPF Real-Time Monitoring** - Detect access at syscall level:
```c
// Trace openat syscall for /proc/*/mem access
TRACEPOINT_PROBE(syscalls, sys_enter_openat) {
char path[256];
bpf_probe_read_user_str(&path, sizeof(path), (void *)ctx->args[1]);
if (strstr(&path, "/proc") && strstr(&path, "mem")) {
events.perf_submit(ctx, &event, sizeof(event));
}
return 0;
}
```
**memdump** - Capture CI/CD runner memory post-execution, scan for credential remnants before process termination.
#### Defensive Countermeasures
```yaml
# Container runtime - deny ptrace capability
securityContext:
capabilities:
drop: [SYS_PTRACE]
readOnlyRootFilesystem: true
```
- Deploy runners in VM isolation (not container-only)
- Enable hardware memory encryption (Intel TME / AMD SEV-SNP)
- Rotate all credentials immediately post-workflow execution
### 3. eBPF Runtime Security (Cross-Vector Detection)
#### Tool Comparison
| Tool | Strength | Best For | Enforcement |
|------|----------|----------|-------------|
| **Falco** | Rules-based alerting | Container environments, broad adoption | Alert only (SIGSTOP可选) |
| **Tetragon** | Kubernetes-native, policy enforcement | K8s clusters, tracing policies | Kill/block via policy |
| **Tracee** | Deep syscall tracing, eBPF-only | Bare-metal, CI/CD runners | Alert + filter |
#### Falco Rules for Supply Chain Attacks
```yaml
- rule: Unauthorized Process Memory Access
condition: >
open and container and
fd.name glob "/proc/*/mem" or syscall == ptrace
output: "Unauthorized memory access (user=%user.name cmd=%proc.name file=%fd.name)"
priority: CRITICAL
tags: [supply-chain, memory_access]
- rule: Suspicious Env Variable Exfiltration
condition: >
syscall == execve and container and
proc.name in (curl, wget, nc, python) and
(proc.args contains "GITHUB_TOKEN" or proc.args contains "ghp_" or proc.args contains "npm_")
priority: CRITICAL
tags: [supply-chain, credential_exfil]
- rule: Suspicious Systemd Service Creation
condition: >
open_write and fd.name glob "*/.config/systemd/user/*" and
not proc.name in (systemctl, systemd, ansible)
priority: HIGH
tags: [supply-chain, persistence]
```
#### Tetragon TracingPolicy (Enforcement)
```yaml
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
name: block-proc-mem-access
spec:
kprobes:
- call: "__fput"
selectors:
- matchArgs:
- index: 0
operator: "Equal"
values: ["/proc/*/mem"]
action: SIGKILL
```
#### Tracee for CI/CD Runner Monitoring
```bash
tracee \
--events=open,openat,ptrace,execve \
--filter='container and (event.args.pathname contains "/proc" and event.args.pathname contains "mem")' \
--output json \
--output-file events.json
```
**Workstation Note**: eBPF tools require kernel support. For this Arch Linux workstation, `bpf` package provides headers. Falco can run in agent mode for local monitoring.
### 4. npm Supply Chain Defense (CanisterWorm Defense)
#### Lifecycle Script Mitigation
```bash
# Global disable (already applied)
npm config set ignore-scripts true
# Per-project override when needed
npm ci --ignore-scripts=false --only=dev # explicit opt-in
# Node.js policy-based sandboxing (experimental)
node --experimental-policy=policy.json install
```
**Limitation**: Shai-Hulud 2.0 uses **preinstall** phase, bypassing postinstall-focused defenses. `--ignore-scripts` blocks both, but some legitimate packages require scripts. Use allowlists:
```json
// policy.json - allow specific packages only
{
"resources": {
"node_modules/.package-lock.json": {
"integrity": true
}
},
"dependencies": {
"package:lodash": true,
"package:express": true
}
}
```
#### Token Security
| Practice | Implementation |
|----------|---------------|
| Granular Access Tokens (GAT) | Scope to specific repos + read-only or automation permissions |
| Token expiration | Set max 90 days; automate rotation |
| Never store in `.npmrc` | Use environment variables or OIDC |
| Audit tokens quarterly | `npm token list`, revoke unused |
| MFA enforcement | Required for package publish (prevents bypass token abuse) |
#### npm OIDC (Long-Term Fix)
npm OIDC integration is "very hard to compromise" and would "almost completely erase supply-chain attack surface" (Chainguard, 2026). Enables tokenless authentication for package operations.
#### Systemd Service Monitoring (CanisterWorm `pgmon`)
```bash
# Detect unknown user services
systemctl --user list-units --type=service --all
# Audit service files for anomalies
ls -la ~/.config/systemd/user/
# Check for services with suspicious exec paths
grep -r "ExecStart" ~/.config/systemd/user/ | grep -v "/usr/bin\|/usr/local/bin"
# Monitor service file creation (eBPF Falco rule above)
```
#### ICP Blockchain C2 Detection
- Decentralized C2 via ICP canisters is resistant to traditional takedown
- Monitor for outbound connections to Dfinity boundary nodes from unexpected processes
- Network baseline: CI/CD runners should never connect to ICP endpoints
- Block ICP boundary node IPs at firewall level for non-ICP workloads
#### Worm Containment
- **socket.dev**: Detects malicious packages within 2-5 minutes of publication via differential analysis
- Monitor all npm packages under org control for unauthorized version publishes
- Alert on rapid-succession version publishes (1-3 day windows = suspicious)
- Revoke and rotate tokens immediately if compromise suspected
### 5. Unicode Attack Defense (GlassWorm Defense)
#### Understanding the Decoder Pattern
GlassWorm uses this exact decoder signature:
```javascript
const s = v => [...v].map(w => (
w = w.codePointAt(0),
w >= 0xFE00 && w <= 0xFE0F ? w - 0xFE00 :
w >= 0xE0100 && w <= 0xE01EF ? w - 0xE0100 + 16 : null
)).filter(n => n !== null);
eval(Buffer.from(s(``)).toString('utf-8'));
```
Detection must target: `eval()` + `Buffer.from()` + codepoint arithmetic + apparent empty/whitespace strings.
#### Detection Tools
```bash
# Variation Selectors (VS1-VS16)
grep -rP "[\x{FE00}-\x{FE0F}]" . --exclude-dir=node_modules --exclude-dir=.git
# Ideographic Variation Selectors
grep -rP "[\x{E0100}-\x{E01EF}]" . --exclude-dir=node_modules --exclude-dir=.git
# Combined with decoder pattern
grep -rP "(codePointAt|fromCharCode|Buffer\.from)" . \
--include="*.js" --include="*.ts" --exclude-dir=node_modules
```
Python scanner for byte-level detection:
```python
#!/usr/bin/env python3
"""Scan files for GlassWorm Unicode injection patterns."""
import sys, pathlib
VS_RANGES = [(0xFE00, 0xFE0F), (0xE0100, 0xE01EF)]
def scan_file(path):
content = path.read_bytes()
hits = []
for i, b in enumerate(content):
# Check for multi-byte UTF-8 sequences in VS ranges
if b >= 0xEF: # Start of 3-byte UTF-8 (VS range)
if i + 2 < len(content):
cp = int.from_bytes(content[i:i+3], 'big')
for start, end in VS_RANGES:
if start <= cp <= end:
hits.append((i, hex(cp)))
return hits
for p in pathlib.Path(sys.argv[1]).rglob('*'):
if p.suffix in ('.js', '.ts', '.mjs', '.cjs', '.py', '.json'):
hits = scan_file(p)
if hits:
print(f"{p}: {len(hits)} invisible chars found")
for offset, cp in hits[:5]:
print(f" offset {offset}: {cp}")
```
#### Git Pre-commit Hook
```yaml
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: no-invisible-unicode
name: Block invisible Unicode (GlassWorm defense)
entry: python3 scripts/unicode-scanner.py
language: script
types: [text]
```
#### IDE Visibility Configuration
**VS Code** - Show invisible characters:
```json
{
"editor.renderWhitespace": "all",
"editor.unicodeHighlight.ambiguousCharacters": true,
"editor.unicodeHighlight.invisibleCharacters": true
}
```
**Vim/Neovim**:
```vim
set list
set listchars+=trail:~,nbsp:␣
" Conceal off for PUA/VS ranges
syntax match GlassWorm "[\uFE00-\uFE0F\uE0100-\uE01EF]" cchar=
```
#### VS Code Extension Security
- Verify extension publisher identity before install
- Audit extension update history for suspicious version jumps
- Known malicious: `quartz.quartz-markdown-editor` v0.3.0 (March 12, 2026)
- 72+ malicious Open VSX extensions in coordinated campaign
- Prefer extensions with signed commits and provenance
- Review extension permissions (workspace access, terminal access = high risk)
#### Semgrep Rules
```yaml
rules:
- id: glassworm-decoder-pattern
patterns:
- pattern-either:
- pattern: |
eval(Buffer.from($FN(...)).toString(...))
- pattern: |
$VAR = $V => [...$V].map($W => ($W = $W.codePointAt(0), ...))
message: "Potential GlassWorm Unicode decoder pattern detected"
severity: ERROR
languages: [javascript, typescript]
```
### 6. SLSA Provenance & Sigstore Ecosystem
#### SLSA Framework (v1.2 Current)
| Level | Requirement | This Workstation |
|-------|-------------|-----------------|
| L1 | Documented build process | Partial (Ansible) |
| L2 | Hosted build platform | N/A (local dev) |
| L3 | Hardened build + provenance | N/A |
| L4 | Hermetic + reproducible | N/A |
**Applicability**: SLSA primarily targets CI/CD builds. For this local workstation, focus on L1 (documented process) and verifying SLSA provenance of consumed packages/artifacts.
#### Sigstore Components
| Component | Purpose | Tool |
|-----------|---------|------|
| **cosign** | Container image signing/verification | `pacman -S cosign` |
| **fulcio** | Certificate authority (issues certs based on OIDC) | Service |
| **rekor** | Transparency log (immutable record) | Service |
| **sigstore-js** | Node.js client library | `pnpm add @sigstore/sign` |
```bash
# Verify a container image was signed
cosign verify --certificate-identity=action@github.com \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
aquasec/trivy:0.69.3
# Sign an artifact
cosign sign --key env://COSIGN_PRIVATE_KEY my-image:latest
```
#### in-toto Attestations
in-toto provides a framework for verifying the integrity of the software supply chain. Key concepts:
- **Layout**: Defines expected steps, authorized functionaries, and inspection rules
- **Attestation**: Metadata about a supply chain step (linked to SLSA provenance)
- **Verification**: Client-side check that all steps match the layout
Integration with GitHub Actions via `slsa-github-generator`:
```yaml
- uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.10.0
with:
attestation-name: my-attestation
```
### 7. SBOM Management
#### Toolchain
```bash
# Generate SBOM (CycloneDX format - preferred for security)
syft packages dir:./src -o cyclonedx-json > sbom.json
# Alternative: SPDX format (preferred for license compliance)
syft packages dir:./src -o spdx-json > sbom.spdx.json
# Scan SBOM for vulnerabilities
grype sbom:sbom.json --fail-on high
```
#### Format Choice
| Format | Best For | Tooling |
|--------|----------|---------|
| **CycloneDX** | Security scanning, vulnerability matching | grype, Dependency-Track |
| **SPDX** | License compliance, legal | SPDX tools, FOSSA |
#### Dependency-Track (SBOM Repository)
Centralized SBOM storage and continuous monitoring:
- Upload SBOMs via API on each build
- Continuous vulnerability scanning (NVD, OSV, GitHub Advisories)
- Policy evaluation (fail builds on critical vulns)
- This workstation: run via Docker for local SBOM management
### 8. Nuclei Template-Based Scanning
```bash
# Install
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
# Scan for known supply chain C2 indicators
nuclei -t supply-chain/ -u scan.aquasecurtiy.org
# Scan local git repos for TeamPCP exfil patterns
nuclei -t custom/teampcp-exfil.yaml -target https://github.com/org/repo
# Custom template for C2 domain detection
cat <<'EOF' > nuclei-teampcp.yaml
id: teampcp-c2-detection
info:
name: TeamPCP C2 Domain
severity: critical
dns:
- name: "{{Hostname}}"
matchers:
- type: word
words:
- "45.148.10.212"
EOF
```
### 9. ML-Based & Behavioral Detection (Emerging)
#### ML Detection Approaches for Supply Chain Attacks
| Technique | Application | Tools/Platforms |
|-----------|-------------|-----------------|
| **Package behavior modeling** | Detect anomalous postinstall behavior vs. historical baseline | socket.dev (differential analysis), EndorLabs |
| **Dependency graph anomaly detection** | Flag sudden dependency additions, version spikes, or transitive changes | Snyk Advisor, Socket.dev risk scores |
| **Code embedding similarity** | Detect AI-generated commits that mimic project style (GlassWorm evasion) | CodeQL ML models, GitHub Advanced Security |
| **Network behavior profiling** | Detect C2 beacon patterns, unusual outbound from build runners | Elastic ML, CrowdStrike Falcon |
| **File system entropy analysis** | Detect encoded payloads (Unicode steganography, encrypted strings) | YARA rules + ML classifiers |
#### Behavioral Detection Patterns
**Anomalous Package Behavior** (CanisterWorm/Shai-Hulud):
- Package executes network calls during install (baseline: most packages don't)
- Package writes files outside `node_modules` during install
- Package reads environment variables or `.npmrc` during install
- Package spawns child processes during install
- Detection: compare current package behavior against historical behavior profile
**Anomalous Commit Patterns** (GlassWorm):
- Commit modifies only whitespace/Unicode but diff shows zero visual changes
- Commit timestamp clusters with other suspicious commits across repos (coordinated campaign signal)
- Commit author style differs from project norm (AI-generated code detection)
- Detection: `git diff --word-diff` to catch invisible char changes; commit metadata clustering
**Anomalous Build Behavior** (TeamPCP):
- Build process accesses `/proc/*/mem` (never legitimate in CI)
- Build process resolves non-whitelisted DNS (tag poisoning C2 indicator)
- Build process creates outbound connections to non-cdn endpoints
- Detection: Falco rules (above), DNS query logging, network egress allowlists
#### Implementation for This Workstation
```bash
# Socket.dev CLI - local package risk scoring
npm install -g socket-cli
socket view express # Risk profile before installing
socket scan ./src # Scan dependencies for behavioral anomalies
# YARA rule for GlassWorm encoded payloads (high entropy strings)
# Install: pacman -S yara
cat <<'EOF' > rules/glassworm.yar
rule GlassWorm_Unicode_Payload {
strings:
$decoder = /codePointAt.*0xFE00|codePointAt.*0xE0100/ ascii
$eval_buf = /eval\(Buffer\.from/ ascii
$vs_range = /\xEF\xB8[\x80-\x8F]/ // UTF-8 for U+FE00-FE0F
condition:
2 of them
}
EOF
yara -r rules/glassworm.yar ./src
```
### 10. DNS & Network Monitoring
#### Passive DNS Integration
- Subscribe to Farsight Security or RiskIQ passive DNS feeds
- Query historical DNS data for infrastructure domains during IR
- Detect CNAME chains pointing to attacker-controlled servers
#### Threat Intelligence Feeds
| Feed | Coverage | Integration |
|------|----------|-------------|
| URLhaus | Known C2 domains | API, daily dump |
| Abuse.ch SSLBL | Malicious SSL certificates | IP blocklists |
| VirusTotal | Domain reputation | API |
| Custom | Typo variants of internal domains | Local script |
#### DNS Monitoring for CI/CD
- Log all DNS queries from runners to SIEM
- Block non-whitelisted external domains from runners
- Alert on edit distance < 2 from known infrastructure domains
---
## Hardening Actions
### Immediate (Already Applied)
```bash
npm config set ignore-scripts true
```
### Recommended for This Workstation
```bash
# 1. Install supply chain security tools
pacman -S syft grype cosign # SBOM, vuln scan, artifact verify
uv tool install semgrep # SAST with Unicode rules
# 2. Set up Unicode scanning as pre-commit hook
# (see GlassWorm section above for hook config)
# 3. Configure IDE visibility
# (see GlassWorm section for VS Code/Vim settings)
# 4. Audit systemd user services monthly
systemctl --user list-units --type=service --all
# 5. Network-level C2 blocking (optional)
# Block 45.148.10.212 and ICP boundary nodes if not needed
```
### CI/CD Hardening Checklist
- [ ] Pin all Actions to SHA256 (never tags)
- [ ] Use OIDC over long-lived secrets
- [ ] Block outbound to non-essential domains from runners
- [ ] Run sensitive builds in ephemeral environments
- [ ] Generate and verify SLSA provenance for built artifacts
- [ ] Sign container images with cosign
- [ ] Generate SBOM on every build (syft + grype)
- [ ] Run Nuclei templates against dependencies
- [ ] Enable Unicode scanning in CI pipeline
---
## Detection Patterns
### File System IOCs
```bash
# CanisterWorm
find ~ -name "pgmon*" -o -name "uv-proxy" -o -name "pglog" 2>/dev/null
# Unknown systemd user services
ls ~/.config/systemd/user/ | grep -v "default.target"
```
### Unicode IOCs (GlassWorm)
```bash
# VS1-VS16 range
grep -rP "[\x{FE00}-\x{FE0F}]" . --exclude-dir=node_modules --exclude-dir=.git
# Ideographic Variation Selectors
grep -rP "[\x{E0100}-\x{E01EF}]" . --exclude-dir=node_modules --exclude-dir=.git
# Decoder function pattern
grep -rP "codePointAt.*0xFE00|codePointAt.*0xE0100" . --include="*.js" --include="*.ts"
```
### Network IOCs
```bash
# TeamPCP C2
ss -ntu | grep -E "45.148.10.212"
getent hosts scan.aquasecurtiy.org
# ICP C2 (unexpected connections to Dfinity)
ss -ntu | grep -E "icp0\.io|dfinity"
```
---
## Workstation-Specific Implementation Priority
| Priority | Action | Effort | Impact |
|----------|--------|--------|--------|
| P0 | Pre-commit hook for Unicode scanning | 30min | Blocks GlassWorm |
| P0 | VS Code invisible char visibility | 5min | Visual detection |
| P1 | Monthly systemd service audit | 10min | Detects CanisterWorm |
| P1 | `npm config set ignore-scripts true` verify | 1min | Blocks script-based attacks |
| P2 | syft/grype SBOM scanning | 1hr setup | Continuous vuln monitoring |
| P2 | Nuclei C2 template scanning | 30min | Network IOC validation |
| P3 | Falco local agent setup | 2hr | Runtime memory/process monitoring |
| P3 | cosign artifact verification workflow | 1hr | Build integrity |
---
## References
- Upwind Security: Trivy/TeamPCP analysis
- EndorLabs: CanisterWorm + SANDWORM_MODE technical analysis
- Aikido Security: GlassWorm Unicode campaign tracking
- Unit42 (Palo Alto): Shai-Hulud 2.0 npm supply chain attack
- Chainguard: npm supply chain hardening and OIDC future
- CISA: GitHub Actions Security Hardening Guidance
- Cilium/Tetragon: eBPF Security Enforcement documentation
- Falco: Runtime security rules and best practices
- SLSA v1.2: slsa.dev/spec/v1.2
- Sigstore: sigstore.dev
- NIST SSDF v1.1: Secure Software Development Framework
+75
View File
@@ -0,0 +1,75 @@
---
description: >-
TRIM policy optimization for the mw-pfeddersheim-workstation after the
Samsung PM9A1 firmware update to GXA7801Q.
tags:
- storage
- trim
- nvme
- ansible
- optimization
last_updated: '2026-04-02'
---
# TRIM Optimization After PM9A1 Firmware Update
## Summary
On 2026-04-02 the workstation TRIM policy was aligned with current Linux best
practice for ext4.
- Root filesystem (`/`) on `/dev/nvme0n1p2` was changed from `noatime,discard`
to `noatime` in `/etc/fstab`
- Weekly `fstrim.timer` remains enabled and active
- Continuous ext4 discard was disabled at runtime with a remount using
`nodiscard`
## Why This Change Was Necessary
The root ext4 filesystem was configured with continuous `discard` while the
system already had periodic TRIM enabled via `fstrim.timer`.
For ext4, continuous discard adds overhead to unlink and truncate operations.
With a healthy weekly `fstrim.timer`, it is redundant on this workstation.
## Implementation
The persistent change was implemented in Ansible.
- `ansible/vars/main.yml`
- added `root_filesystem_uuid`
- added `root_filesystem_mount_opts: noatime`
- `ansible/roles/common/tasks/main.yml`
- ensured the root fstab entry is managed with `mount: state=present`
- checked current root mount options with `findmnt`
- remounted `/` without continuous discard when needed
## Verification
### fstab
```text
UUID=2aeade2e-b169-4964-8cbd-bb7356320b51 / ext4 noatime 0 1
```
### Periodic TRIM
```text
ActiveState=active
UnitFileState=enabled
NextElapseUSecRealtime=Mon 2026-04-06 00:49:28 CEST
```
### Validation Steps
```text
ansible-playbook ansible/workstation.yml --check --tags trim
ansible-playbook ansible/workstation.yml --tags trim
findmnt --verify --tab-file /etc/fstab
```
## Notes
- `/boot/efi` remains unchanged
- This change only targets the ext4 root filesystem TRIM policy
- Firmware and benchmark documentation remain in the NVMe benchmark documents
+78 -22
View File
@@ -7,32 +7,88 @@ tags:
- health-check
- workstation
- monitoring
last_updated: '2026-03-13'
last_updated: '2026-06-17'
---
# Workstation Health Report: mw-manjaro-pf
## System Specification Baseline
- **CPU**: AMD Ryzen 7 2700X (16 Threads) @ 3.9 GHz Max
- **Memory**: 62 GiB RAM
- **Primary Disk**: 512 GB NVMe SSD (SAMSUNG MZVL2512HCJQ)
## System Specifications
- **CPU**: AMD Ryzen 7 2700X (16 threads) @ 3.9GHz max
- **Memory**: 64 GiB DDR4-2666 MT/s (4x 16 GiB, 2x Kingston + 2x Samsung)
- **Primary Disk**: 512 GB NVMe SSD (Samsung MZvl2512HCJQ)
- **Secondary Disk**: 500 GB SATA SSD (Samsung 840 EVO) -> `/home/mw/models`
- **OS**: Manjaro Linux (Kernel 6.12)
## Current Health Status (2026-03-13)
- **Uptime**: 13 days, 19 hours
- **Load Average**: 1.59, 1.46, 1.23 (16 Threads)
- **Memory**: 28/62 GiB (44% used)
- **Disk Usage (/)**: 84% (371G used, 74G free)
- **Maintenance**:
- Orphaned packages detected (cmake, nasm, patchelf, etc.)
- Logs vacuumed (88M journal)
- Docker environment verified (6 active containers)
- **Failed Services**: `archlinux-keyring-wkd-sync.service` (failed)
- **Network**: tailscale0 active (100.64.0.49), enp5s0 active (192.168.0.5)
## Current Health Status (2026-03-17 23:21 CET)
## Critical Runtimes
- Python: 3.14.3
- Node.js: v25.6.1
- Ansible: core 2.20.2
- Docker: 29.2.1
- **Uptime**: ~1 hour, 7 minutes (post-reboot due to OOM crash at 18:33)
- **Load Average**: 0.40, 0.50, 1.45 (very low)
- **Memory**: 7.9 GiB used / 62 GiB total (12%)
- **Swap**: 0 B used / 16 GiB (0%)
- **zswap**: Active (zstd/zsmalloc, 30% pool)
- **Disk Usage (/)**: 84% (371G used, 74G free)
- **OOM Protection**: `systemd-oomd` active, monitoring `/user.slice`
- **Kernel Optimizations**:
- mglru: Active
- Transparent hugepages: `madvise`
- Proactive reclaim: `watermark_scale_factor 100`
- Dirty bytes: `dirty_ratio=10`, `dirty_background_ratio=5`
- **Maintenance**:
- Crash analysis performed for 2026-03-17 incident
- S.M.A.R.T. hardware check passed for NVMe
## Crashlog Analysis (2026-03-17)
### Recent Errors (from journalctl)
No errors in the last hour from `journalctl -p err`
### OOM Events
- **15:06:06 CET**: Node.js process (PID 122570, 122573, 122574, 122575, 122625) dumped core due to OOM
- These appear to be Cline/agent-related processes that exhausted memory
- Systemd-oomd correctly intervened and terminated the processes
### Kernel Warnings (Boot)
- **VMSCAPE**: SMT advisory - mitigation available (enable STIBP for full protection)
- **ATA link failures**: ata3/4/7/8 failed to resume (idle SATA ports)
- **Samsung 840 EVO quirks**: noncqtrim, zeroaftertrim, nodmalog (normal for older SSD)
- **NVIDIA taint**: Proprietary driver (expected)
- **UVC webcam warnings**: Non-critical (device firmware)
- **Pipewire Broken Pipe**: Video device initialization issues (non-critical)
### Failed Services
- `archlinux-keyring-wkd-sync.service`: Failed (non-critical, keyring sync)
### Docker
- **Status**: 6 containers running
- **No zombie containers detected**
### Health Check Script Results
```text
Root partition: 78% used
Git status: Dirty (34 changes) - workstation-health.md modified
Docker: 6 containers running
Uptime: ~1 hour, 7 minutes
```
## Recommendations
1. **Monitor Node.js memory**: Today's OOM crash involved multiple Cline-related node processes.
Consider investigating if agent sessions are consuming excessive memory.
2. **Replace Samsung 840 EVO**: Currently applies SATA quirks (noncqtrim, zeroaftertrim, nodmalog).
Action: No action needed (non-critical, working as expected)
3. **Monitor memory**: 12% usage is very healthy. Current state is stable.
## 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)
## 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.
+10
View File
@@ -0,0 +1,10 @@
# End of Day (EOD) - Hygiene & Git
Focuses on code quality, repository cleanliness, commit strategy, and pipeline health.
## Key Tasks
- [ ] **Run Tests**: Ensure all tests pass.
- [ ] **Linter Check**: Verify that the codebase is clean and follows standards.
- [ ] **Git Status**: Check for untracked or modified files.
- [ ] **Atomic Commits**: Ensure each commit represents a single logical change.
- [ ] **WIP Cleanup**: Delete temporary or stale branches.
- [ ] **Push to Remote**: Ensure all local work is backed up.
@@ -0,0 +1,9 @@
# End of Day (EOD) - Knowledge & Documentation
Focuses on preserving context, updating docs, and ensuring smooth resume/handoff.
## Key Tasks
- [ ] **README / CHANGELOG**: Document major changes or achievements.
- [ ] **NEXT_STEPS.md**: Define the immediate priorities for the next session.
- [ ] **ADRs**: Capture any significant architectural decisions.
- [ ] **Learnings**: Document any new insights or findings.
- [ ] **Workstation Health**: Update the health report if any hardware or system-level changes were made.
+9
View File
@@ -0,0 +1,9 @@
# End of Day (EOD) - Ops & Automation
Focuses on cost tracking, environment cleanup, and automated execution.
## Key Tasks
- [ ] **Run Maintenance**: Execute `scripts/maintenance.sh` to clean system logs and caches.
- [ ] **Docker Cleanup**: Prune unused Docker images, containers, and volumes.
- [ ] **Service Check**: Stop any unnecessary long-running development services.
- [ ] **Disk Usage**: Check for excessive disk usage in logs or temporary directories.
- [ ] **Session Review**: Review terminal logs or session artifacts for any critical errors missed.
+494
View File
@@ -0,0 +1,494 @@
---
description: >-
Arch Linux workstation hardening workflow with daily, weekly, and monthly
security checks based on March 2026 supply chain threat landscape.
tags:
- arch-linux
- hardening
- security
- supply-chain
- workflow
last_updated: '2026-03-24'
priority: 8
---
# Workstation Hardening Workflow (Arch Linux)
**Source**: `docs/tech/supply-chain-security-2026-03.md`
## Schedule Overview
| Cadence | Duration | Focus |
|---------|----------|-------|
| Daily | 5 min | Service health, network, recent changes |
| Weekly | 15 min | Package audit, SBOM scan, journal review |
| Monthly | 30 min | IOC sweep, persistence audit, credential rotation |
---
## Daily Checks (5 min)
### 1. Systemd Service Health
```bash
# Failed services
systemctl --failed
# Unexpected user services (CanisterWorm pgmon detection)
systemctl --user list-units --type=service --all
```
**Alert if**: Unknown services present, especially short names like `pgmon`.
### 2. Network Connections
```bash
# Established outbound connections
ss -ntu state established
# TeamPCP C2 check
ss -ntu | grep -E "45.148.10.212"
# ICP C2 check (unexpected Dfinity connections)
ss -ntu | grep -E "icp0\.io|dfinity"
```
**Alert if**: Connections to unknown external IPs, especially C2 indicators.
### 3. Boot Performance & Service Timing
```bash
# Boot time
systemd-analyze
# Slowest services (flag if >1s unexpected)
systemd-analyze blame | head -10
# Critical chain
systemd-analyze critical-chain
```
**Alert if**: New services in critical chain, unexpected boot time increase.
### 4. Recent Package Changes
```bash
# Packages changed in last 24h
pacman -Q --explicit | while read pkg ver; do
stat -c '%Y %n' /var/lib/pacman/local/$pkg-* 2>/dev/null
done | awk '$1 > '$(date -d 'yesterday' +%s)' {print}'
```
**Alert if**: Unexpected package installations or updates.
---
## Weekly Audit (15 min)
### 1. Package Orphan Cleanup
```bash
# List orphans (no longer required)
pacman -Qdt
# Remove orphans (review first)
pacman -Rns $(pacman -Qdtq)
```
### 2. npm/pnpm Audit
```bash
# Verify scripts disabled
npm config get ignore-scripts # expect: true
# Audit all active projects
fd -t f -e json -p 'package-lock' ~/internal ~/external 2>/dev/null | while read lock; do
dir=$(dirname "$lock")
echo "=== $dir ==="
(cd "$dir" && pnpm audit 2>&1 | head -20)
done
```
**Alert if**: `ignore-scripts` is not `true`, high/critical vulnerabilities.
### 3. SBOM Vulnerability Scan
```bash
# Generate and scan SBOM for active projects
fd -t d -p 'node_modules' ~/internal ~/external 2>/dev/null | while read dir; do
project=$(dirname "$dir")
echo "=== $project ==="
syft packages dir:"$project" -o cyclonedx-json 2>/dev/null | grype sbom:/dev/stdin --fail-on high 2>&1 | tail -5
done
```
**Alert if**: High/critical CVEs in dependencies.
### 4. Semgrep SAST Scan
```bash
# Scan all repos for supply chain rules
fd -t d -p '.git' ~/internal ~/external 2>/dev/null | while read gitdir; do
repo=$(dirname "$gitdir")
echo "=== $repo ==="
(cd "$repo" && semgrep --config p/supply-chain --config p/javascript --error 2>&1 | tail -5)
done
```
**Alert if**: Findings in `eval`, `child_process`, `Buffer.from`, prototype pollution.
### 5. SHA256 Package Verification
```bash
# Verify pacman package integrity against known-good hashes
pacman -Qk 2>&1 | grep -v "0 modified files"
# Check for package downgrade attacks
pacman -Qi linux | grep -E "Install Date|Version"
```
**Alert if**: Modified files in package, unexpected version downgrade.
### 6. Journal Suspicious Activity
```bash
# Last 7 days: failed auth, suspicious exec, ptrace
journalctl --since "7 days ago" -p err --no-pager | grep -iE "denied|ptrace|segfault|oom"
# Suspicious process starts from non-standard paths
journalctl --since "7 days ago" --no-pager | grep -E "ExecStart.*\.local" | grep -v "systemd"
```
**Alert if**: Repeated auth failures, ptrace from unexpected processes.
---
## Monthly Deep Scan (30 min)
### 1. Unicode Injection Scan (GlassWorm)
```bash
# Scan all code repos for invisible Unicode
fd -t f -e js -e ts -e mjs -e py ~/internal ~/external 2>/dev/null | while read f; do
# Variation Selectors (U+FE00-FE0F)
grep -Pl "[\x{FE00}-\x{FE0F}]" "$f" 2>/dev/null && echo "VS_RANGE: $f"
# Ideographic Variation Selectors (U+E0100-E01EF)
grep -Pl "[\x{E0100}-\x{E01EF}]" "$f" 2>/dev/null && echo "IVS_RANGE: $f"
# Decoder pattern
grep -Pl "codePointAt.*0xFE00|codePointAt.*0xE0100|eval\(Buffer\.from" "$f" 2>/dev/null && echo "DECODER: $f"
done
```
**Alert if**: Any matches = immediate investigation, likely compromise.
### 2. Systemd Persistence Audit (CanisterWorm)
```bash
# List all user service files
ls -la ~/.config/systemd/user/
# Check for suspicious ExecStart paths (not /usr/bin or /usr/local)
grep -r "ExecStart" ~/.config/systemd/user/ 2>/dev/null | grep -v "/usr/bin\|/usr/local/bin"
# Check for worm artifacts
find ~ -maxdepth 3 -name "pgmon*" -o -name "uv-proxy" -o -name "pglog" 2>/dev/null
```
**Alert if**: Unknown services, ExecStart pointing to dotfiles or temp dirs.
### 3. IOC Sweep
```bash
# TeamPCP exfil repos
gh repo list --limit 100 --json name 2>/dev/null | jq -r '.[].name' | grep -i "tpcp-docs"
# DNS C2 resolution check
getent hosts scan.aquasecurtiy.org 2>/dev/null
# Known malicious extension check
code --list-extensions 2>/dev/null | grep -i "quartz.quartz"
```
**Alert if**: Any IOC matches = incident response protocol.
### 4. Credential Rotation Check
```bash
# npm tokens older than 90 days
npm token list 2>/dev/null
# GitHub tokens - check for long-lived PATs
gh auth status 2>/dev/null
# SSH keys - check for keys without passphrase
ssh-add -l 2>/dev/null
```
**Action**: Rotate any token older than 90 days. Enforce GAT scoping.
### OIDC & Token Best Practices
| Practice | Implementation |
|----------|---------------|
| **OIDC over PAT** | Use `gh auth login` with browser flow, not personal access tokens |
| **GAT scoping** | `gh auth refresh -s repo,read:packages` (minimum scopes) |
| **npm OIDC** | Configure `npm-provenance` in CI, avoid `NPM_TOKEN` env vars |
| **Short-lived tokens** | CI tokens max 1 hour TTL, no long-lived secrets in repos |
| **Secret scanning** | Enable GitHub secret scanning + push protection |
### 5. YARA Malware Pattern Scan
```bash
# Scan home directories with YARA rules (see source doc for rule files)
yara -r /path/to/rules/malware.yar ~/internal ~/external 2>/dev/null
# Quick inline check for known worm patterns
grep -rl "pgmon\|uv-proxy\|pglog" ~/internal ~/external 2>/dev/null
grep -rl "codePointAt.*0xFE00\|0xE0100" ~/internal ~/external 2>/dev/null
```
**Alert if**: Any YARA rule match = incident response protocol.
### 6. Cosign Artifact Verification
```bash
# Verify container images before use (example)
cosign verify --key cosign.pub ghcr.io/org/image:tag 2>/dev/null
# Verify with OIDC keyless
cosign verify --certificate-identity=*.satware.ai --certificate-oidc-issuer=https://token.actions.githubusercontent.com ghcr.io/org/image:tag 2>/dev/null
```
**Alert if**: Signature verification fails, do not use the artifact.
### 7. Nuclei Network Template Scan
```bash
# Scan local services for known vulnerabilities
nuclei -u http://localhost -t cves/ -severity high,critical -silent 2>/dev/null
# Scan specific ports
nuclei -u http://localhost:3000 -t exposures/ -silent 2>/dev/null
```
**Alert if**: High/critical template matches on local services.
### 8. Full SBOM Regenerate
```bash
# Regenerate SBOMs for all active projects
fd -t f -e json -p 'package-lock' ~/internal ~/external 2>/dev/null | while read lock; do
dir=$(dirname "$lock")
syft packages dir:"$dir" -o cyclonedx-json > "$dir/sbom.cyclonedx.json" 2>/dev/null
grype sbom:"$dir/sbom.cyclonedx.json" --fail-on high > "$dir/grype-report.txt" 2>&1
echo "$dir: $(grep -c 'High\|Critical' "$dir/grype-report.txt" 2>/dev/null || echo 0) findings"
done
```
---
## Kernel Hardening Check (Monthly)
```bash
# Verify kernel parameters
sysctl kernel.kptr_restrict # expect: 1 (hide kernel pointers)
sysctl kernel.dmesg_restrict # expect: 1 (restrict dmesg)
sysctl kernel.unprivileged_bpf_disabled # expect: 1 (restrict eBPF)
sysctl vm.mmap_min_addr # expect: 65536 (prevent null mapping)
# Verify ASLR
cat /proc/sys/kernel/randomize_va_space # expect: 2 (full randomization)
# Check loaded kernel modules for suspicious entries
lsmod | awk '{print $1}' | sort
# Verify IOMMU enabled (hardware memory protection)
dmesg | grep -i "DMAR\|IOMMU" | head -3
```
**Alert if**: Any parameter is 0/unset, unknown kernel modules loaded.
## AppArmor/SELinux Status Check (Monthly)
```bash
# AppArmor (Arch default)
sudo aa-status 2>/dev/null | head -20
# Verify profiles are in enforce mode (not complain)
sudo aa-status 2>/dev/null | grep -c "enforce" # should be > 0
sudo aa-status 2>/dev/null | grep "complain" # should be empty
# If using SELinux (alternative)
getenforce 2>/dev/null # expect: Enforcing
sestatus 2>/dev/null
```
**Alert if**: No profiles loaded, profiles in complain mode, SELinux permissive.
## Systemd Sandboxing Audit (Monthly)
```bash
# Check which services lack sandboxing directives
for svc in $(systemctl list-units --type=service --state=running -q --no-pager | awk '{print $1}'); do
has_sandbox=$(systemctl show "$svc" --property=ProtectSystem,ProtectHome,PrivateTmp,NoNewPrivileges,ReadOnlyPaths 2>/dev/null | grep -v "^$" | wc -l)
if [ "$has_sandbox" -lt 3 ]; then
echo "WEAK_SANDBOX: $svc ($has_sandbox directives)"
fi
done
# Audit user services for sandboxing
systemctl --user show '*.service' --property=ProtectSystem,ProtectHome,PrivateTmp 2>/dev/null | grep -B1 "^$"
```
**Alert if**: Critical services (docker, sshd, network) lack sandboxing directives. See source doc for per-service hardening.
## Runtime Monitoring (Falco/eBPF) (Ongoing)
```bash
# Check Falco daemon status (if deployed)
systemctl status falco 2>/dev/null | head -5
# Review recent Falco alerts
journalctl -u falco --since "7 days ago" --no-pager 2>/dev/null | grep -E "Warning|Critical"
# Alternative: check Tetragon if deployed
kubectl get events --field-selector type=Warning 2>/dev/null | tail -10
```
**Alert if**: Falco not running, high-severity alerts in logs. See source doc for Falco rules and Tetragon TracingPolicy YAML.
## Memory Forensics (Incident Response Only)
```bash
# Create memory dump (requires elevated privileges)
sudo dd if=/dev/mem of=/tmp/memdump.raw bs=1M count=1024 2>/dev/null
# Analyze with Volatility 3
uv tool install volatility3
vol -f /tmp/memdump.raw linux.pslist 2>/dev/null
vol -f /tmp/memdump.raw linux.bash 2>/dev/null
# TeamPCP memory theft detection
vol -f /tmp/memdump.raw linux.check_syscall 2>/dev/null
# Cleanup
rm -f /tmp/memdump.raw
```
**Use only**: During active incident investigation. Requires prior setup.
## Firewall Status Check (Weekly)
```bash
# Check nftables ruleset
sudo nft list ruleset 2>/dev/null | head -30
# If using ufw instead
sudo ufw status verbose 2>/dev/null
# Verify default policies (should be DROP for input)
# nftables: look for "type filter hook input priority 0; policy drop"
# ufw: look for "Default: deny (incoming)"
# Check for unexpected open ports
ss -tlnp | grep -E "LISTEN"
```
**Alert if**: Default input policy is ACCEPT, unexpected listening ports, no firewall active.
## Ansible Integration
Persistent hardening changes go through `ansible/` per `.clinerules/workstation.md`.
```bash
# Verify current hardening state
ansible-playbook ansible/workstation.yml --check
# Apply hardening changes
ansible-playbook ansible/workstation.yml
```
**Required Ansible tasks** (add to `ansible/roles/common/tasks/main.yml`):
- Kernel sysctl parameters (kptr_restrict, dmesg_restrict, ASLR)
- Firewall default policies (nftables DROP input)
- AppArmor profile installation
- Systemd sandboxing directives for critical services
- `ignore-scripts: true` for npm config
**Rule**: Never apply sysctl or firewall changes manually. Always via Ansible.
## Tool Installation (One-Time)
```bash
# SBOM, vulnerability scanning, artifact verification, malware patterns
pacman -S syft grype cosign yara
# AppArmor user-space tools
sudo pacman -S apparmor apparmor-profiles
# SAST with supply chain rules
uv tool install semgrep
# Network template scanning
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
# Memory forensics (incident response)
uv tool install volatility3
# npm package risk scoring
npm install -g socket-cli
```
## Pre-commit Hook Setup (One-Time)
Add to `.pre-commit-config.yaml` in each repo:
```yaml
repos:
- repo: local
hooks:
- id: no-invisible-unicode
name: Block invisible Unicode (GlassWorm defense)
entry: python3 scripts/unicode-scanner.py
language: script
types: [text]
```
Place the Unicode scanner at `scripts/unicode-scanner.py` (see source doc for full script).
## IDE Hardening (One-Time)
VS Code `settings.json`:
```json
{
"editor.renderWhitespace": "all",
"editor.unicodeHighlight.ambiguousCharacters": true,
"editor.unicodeHighlight.invisibleCharacters": true
}
```
---
## Incident Response Quick Reference
| Indicator | Action |
|-----------|--------|
| Unknown systemd service | Stop + investigate + remove |
| C2 IP in `ss` output | Kill connection, firewall block, full IOC sweep |
| Unicode IOC in code | Isolate repo, check git history, revert commit |
| Malicious extension | Uninstall immediately, clear extension cache |
| npm token compromise | Revoke token, rotate all tokens, audit publish log |
## Threat Context (March 2026)
| Threat | Vector | Detection |
|--------|--------|-----------|
| **TeamPCP** | Tag poisoning, `/proc/pid/mem` theft | SHA pinning, Falco eBPF |
| **CanisterWorm** | npm worm, systemd persistence | Service audit, npm script block |
| **GlassWorm** | Unicode steganography, invisible chars | grep VS ranges, pre-commit hook |
## Full Reference
Detailed technical background, Falco rules, Tetragon policies, Semgrep rules, YARA rules, and Python scanner: `docs/tech/supply-chain-security-2026-03.md`
+22
View File
@@ -0,0 +1,22 @@
# quarto.yml - Batch PDF conversion config
# Use: quarto render docs/*.md --to typst-pdf
# Or: quarto render docs/md-to-pdf-combo.md --to typst-pdf
project:
type: default
output-dir: "./docs/pdf-output"
format:
typst-pdf:
default: true
toc: false
number-sections: true
mainfont: "Inter"
monofont: "JetBrains Mono"
fontsize: 9pt
papersize: a4
margin:
top: 1.5cm
bottom: 1.5cm
left: 1.5cm
right: 1.5cm
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env bash
# batch-pdf.sh - Convert all Markdown docs to professional PDFs via Pandoc + WeasyPrint
# Usage: bash scripts/batch-pdf.sh [directory]
# Default directory: docs/
#
# Pipeline: pandoc md → HTML → weasyprint CSS → PDF
# Requires: pandoc, weasyprint, CSS file at docs/pdf-comparison/cheatsheet.css
set -euo pipefail
PROJ_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CSS_PATH="${PROJ_ROOT}/docs/pdf-comparison/cheatsheet.css"
DOC_DIR="${1:-${PROJ_ROOT}/docs}"
OUTPUT_DIR="${PROJ_ROOT}/docs/pdf-output"
# Verify dependencies
for cmd in pandoc weasyprint; do
command -v "$cmd" &>/dev/null || { echo "ERROR: $cmd not found. Install: sudo pacman -S $cmd"; exit 1; }
done
[[ -f "$CSS_PATH" ]] || { echo "ERROR: CSS not found at $CSS_PATH"; exit 1; }
mkdir -p "${OUTPUT_DIR}"
count=0
errors=0
tmpdir=$(mktemp -d)
echo "=== Markdown → PDF Batch (Pandoc + WeasyPrint + CSS) ==="
echo "CSS: ${CSS_PATH}"
echo "Input: ${DOC_DIR}"
echo "Output: ${OUTPUT_DIR}"
echo ""
find "${DOC_DIR}" -name "*.md" -not -path "*/pdf-output/*" -not -path "*/.*" -not -name "hyprland-keybindings-no-meta.md" | sort | while read -r md_file; do
rel_path="${md_file#${DOC_DIR}/}"
rel_dir="$(dirname "${rel_path#${DOC_DIR#${PROJ_ROOT}/}/}")"
basename="$(basename "${md_file}" .md)"
title="${basename//-/ }"
out_subdir="${OUTPUT_DIR}"
if [[ "${rel_dir}" != "." && -n "${rel_dir}" ]]; then
out_subdir="${OUTPUT_DIR}/${rel_dir}"
fi
mkdir -p "${out_subdir}"
html_file="${tmpdir}/${basename}.html"
pdf_file="${out_subdir}/${basename}.pdf"
echo " ${rel_path} ..."
pandoc "${md_file}" -t html5 --standalone \
--css "file://${CSS_PATH}" \
--metadata title="${title}" \
-o "${html_file}" 2>&1 || { echo " PANDOC ERROR"; errors=$((errors+1)); continue; }
weasyprint "${html_file}" "${pdf_file}" 2>/dev/null || \
weasyprint "${html_file}" "${pdf_file}" 2>&1 | head -1
if [[ -f "${pdf_file}" ]]; then
size=$(du -h "${pdf_file}" | cut -f1)
echo " OK (${size})"
count=$((count + 1))
else
echo " PDF GENERATION ERROR"
errors=$((errors + 1))
fi
done
rm -rf "${tmpdir}"
echo ""
done_count=$(find "${OUTPUT_DIR}" -name "*.pdf" | wc -l)
echo "=== Done: ${done_count} files in ${OUTPUT_DIR} ==="
+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
+201
View File
@@ -0,0 +1,201 @@
#!/bin/bash
set -euo pipefail
# saTway Daily Routine - 7-phase morning workflow
# Implements "Morning Start Protocol (15 Min)"
mkdir -p "$HOME/logs" 2>/dev/null || true
LOCAL_LOG="$HOME/logs/daily-routine-$(date +%Y%m%d).log"
HEALTH_SCRIPT="$(dirname "$0")/morning-health-check.sh"
MAINTENANCE_SCRIPT="$(dirname "$0")/maintenance.sh"
WORKSPACE="$HOME/internal/mw-pfeddersheim-workstation"
# ANSI colors
RED='\033[0;31m'
YELLOW='\033[0;33m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m'
log() {
echo -e "$1" | tee -a "$LOCAL_LOG"
}
phase_header() {
log "\n${BLUE}--- Phase $1: $2 ---${NC}"
}
run_health_check() {
log "Running health check..."
if [ -x "$HEALTH_SCRIPT" ]; then
HEALTH_EXIT=0
# Disable set -e for the health check call
set +e
bash "$HEALTH_SCRIPT" 2>&1 | tee -a "$LOCAL_LOG"
HEALTH_EXIT=$?
set -e
if [ "$HEALTH_EXIT" -eq 2 ]; then
log "\n${RED}CRITICAL: Health check failed with critical errors.${NC}"
# Still proceed for the morning-full routine but with a big warning?
# Or exit? Let's follow the previous script logic for health:
# morning-full should probably still show the rest of the protocol
# so the user can see everything.
elif [ "$HEALTH_EXIT" -eq 1 ]; then
log "\n${YELLOW}WARNING: Health check finished with warnings.${NC}"
fi
else
log "${YELLOW}WARNING: Health check script not found or not executable${NC}"
fi
}
phase_0_readiness() {
phase_header "0" "Physical & Mental Readiness"
log "Checklist:"
log " [ ] Hydrated"
log " [ ] Light movement"
log " [ ] 1-minute mindfulness"
log " [ ] Time block established"
log "\nActivating DND (macOS)..."
if [[ "$OSTYPE" == "darwin"* ]]; then
osascript -e 'do shell script "defaults write com.apple.controlcenter \"NSStatusItem Visible DoNotDisturb\" -bool true"' || log " Failed to set DND"
pkill -f "Slack" 2>/dev/null || true
pkill -f "Discord" 2>/dev/null || true
log " DND enabled and distractions closed."
else
log " SKIP: Not on macOS"
fi
}
phase_1_context() {
phase_header "1" "Context Restoration"
log "Recent WIP commits:"
git -C "$WORKSPACE" log -5 --pretty=format:"%h %s" --grep="wip:" || log " No WIP commits found"
log "\nRecent learnings:"
ls -lt "$WORKSPACE/docs/learnings/" 2>/dev/null | head -3 || log " No learnings found"
log "\nGit status:"
git -C "$WORKSPACE" status --short
}
phase_2_validation() {
phase_header "2" "Environment Validation"
run_health_check
}
phase_3_priority() {
phase_header "3" "Priority Framework (MoSCoW)"
log "Define exactly 3 priorities. No more."
log " 1. [MUST] _____________________"
log " 2. [MUST/SHOULD] ______________"
log " 3. [SHOULD] ___________________"
}
phase_4_sync() {
phase_header "4" "Team Synchronization"
log " [ ] Async Standup: Post yesterday, today, blockers, availability."
log " [ ] Block Focus Time: Calendar updated."
}
phase_5_warmup() {
phase_header "5" "Cognitive Warmup"
log "Choose ONE (2-5 minutes):"
log " - Review simple PR"
log " - Fix trivial lint warning"
log " - Write small test case"
log " - Document one function"
}
phase_6_ai_setup() {
phase_header "6" "AI Setup"
if command -v cline &> /dev/null; then
log "cline version:"
cline --version | head -n 1 | tee -a "$LOCAL_LOG"
else
log "${YELLOW}WARNING: cline CLI not found${NC}"
fi
}
phase_7_launch() {
phase_header "7" "Launch Deep Work"
log "Ready to start?"
log " 1. Create branch: git checkout -b feat/\$(date +%Y%m%d)_morning_start"
log " 2. Start timer: timer 90m \"Deep work complete!\" &"
log " 3. FIRST ACTION: Write ONE thing that proves work has started (e.g., 1 failing test)."
}
# --- EOD Protocol Phases ---
eod_hygiene_git() {
phase_header "EOD-1" "Git, Hygiene & Verification"
log "Git status:"
git -C "$WORKSPACE" status --short || log "${RED} Git status failed${NC}"
log "\nChecking for large untracked files..."
find "$WORKSPACE" -maxdepth 2 -not -path '*/.*' -size +10M -ls || true
log "\nValidation Checklist:"
log " [ ] Tests passed (run manually if needed)"
log " [ ] No temporary files or logs left behind"
log " [ ] Work committed (including WIP if necessary)"
}
eod_knowledge_documentation() {
phase_header "EOD-2" "Knowledge & Documentation"
log "Documentation Checklist:"
log " [ ] README updated with major achievements"
log " [ ] CHANGELOG updated"
log " [ ] NEXT_STEPS.md updated for tomorrow"
log " [ ] Learnings captured in docs/learnings/"
}
eod_ops_automation() {
phase_header "EOD-3" "Ops & Automation"
log "Running system maintenance..."
if [ -x "$MAINTENANCE_SCRIPT" ]; then
bash "$MAINTENANCE_SCRIPT" | tee -a "$LOCAL_LOG"
else
log "${YELLOW}WARNING: Maintenance script not found or not executable${NC}"
fi
log "\nFinal Ops Checklist:"
log " [ ] Services stopped"
log " [ ] Docker pruned (via maintenance.sh)"
}
# Command line interface
case "${1:-morning}" in
"health")
run_health_check
;;
"morning")
log "--- Starting Lightweight Daily Routine $(date) ---"
phase_2_validation
log "\n--- Daily Routine Finished $(date) ---"
;;
"morning-full")
log "--- Starting Full Morning Protocol $(date) ---"
phase_0_readiness
phase_1_context
phase_2_validation
phase_3_priority
phase_4_sync
phase_5_warmup
phase_6_ai_setup
phase_7_launch
log "\n--- Full Morning Protocol Finished $(date) ---"
;;
"eod")
log "--- Starting End of Day Protocol $(date) ---"
eod_hygiene_git
eod_knowledge_documentation
eod_ops_automation
log "\n--- End of Day Protocol Finished $(date) ---"
;;
*)
echo "Usage: $0 {health|morning|morning-full|eod}"
exit 1
;;
esac
+116
View File
@@ -0,0 +1,116 @@
#!/bin/bash
# install-hyprland.sh - Install Hyprland with Wayland on Manjaro (NVIDIA)
# Run: bash scripts/install-hyprland.sh
set -euo pipefail
echo "=== Hyprland Installation for mw-manjaro-pf ==="
echo "GPU: NVIDIA GTX 1050 Ti (595.71.05)"
echo "Monitors: 3x (DP-1 + HDMI-A-1 + DVI-D-1)"
echo ""
# Step 1: Install core packages
echo "[1/5] Installing Hyprland and core Wayland packages..."
sudo pacman -S --needed --noconfirm \
hyprland \
hyprlock \
hypridle \
hyprshot \
hyprcursor \
xdg-desktop-portal-hyprland \
waybar \
wofi \
dunst \
kitty \
cliphist \
wl-clipboard \
brightnessctl \
pipewire \
pipewire-pulse \
wireplumber \
pavucontrol
echo "[1/5] Done."
# Step 2: NVIDIA-specific package verification
echo "[2/5] Verifying NVIDIA packages..."
pacman -Q nvidia-utils >/dev/null 2>&1 && echo " nvidia-utils: OK" || echo " nvidia-utils: MISSING"
pacman -Q egl-wayland >/dev/null 2>&1 && echo " egl-wayland: OK" || echo " egl-wayland: MISSING"
echo "[2/5] Done."
# Step 3: Config directory (already created)
echo "[3/5] Verifying configuration..."
if [ -f ~/.config/hypr/hyprland.conf ]; then
echo " hyprland.conf: OK ($(wc -l < ~/.config/hypr/hyprland.conf) lines)"
else
echo " hyprland.conf: MISSING - create it first!"
exit 1
fi
echo "[3/5] Done."
# Step 4: Enable DRM KMS for NVIDIA (critical for Wayland)
echo "[4/5] Configuring NVIDIA DRM KMS..."
if grep -q "nvidia-drm.modeset=1" /etc/modprobe.d/*.conf 2>/dev/null; then
echo " nvidia-drm.modeset=1: Already configured"
else
echo " Adding nvidia-drm.modeset=1 to /etc/modprobe.d/nvidia.conf..."
echo "options nvidia-drm modeset=1" | sudo tee -a /etc/modprobe.d/nvidia.conf
echo " IMPORTANT: Reboot required after installation."
fi
# Early loading of NVIDIA modules
if grep -q "nvidia" /etc/mkinitcpio.conf 2>/dev/null; then
echo " mkinitcpio modules: Already configured"
else
echo " Adding NVIDIA modules to mkinitcpio..."
sudo sed -i 's/^MODULES=(/MODULES=(nvidia nvidia_modeset nvidia_uvm nvidia_drm /' /etc/mkinitcpio.conf
sudo mkinitcpio -P
fi
echo "[4/5] Done."
# Step 5: Register Hyprland session
echo "[5/5] Verifying session registration..."
if [ -f /usr/share/wayland-sessions/hyprland.desktop ]; then
echo " Session file: OK"
else
echo " Session file: Creating..."
sudo tee /usr/share/wayland-sessions/hyprland.desktop > /dev/null << 'DESKTOP'
[Desktop Entry]
Name=Hyprland
Comment=An intelligent dynamic tiling Wayland compositor
Exec=Hyprland
Type=Application
DesktopNames=Hyprland
DESKTOP
fi
echo "[5/5] Done."
echo ""
echo "=== Installation Complete ==="
echo ""
echo "NEXT STEPS:"
echo " 1. REBOOT to apply NVIDIA DRM KMS changes"
echo " 2. Select 'Hyprland' from the login manager (SDDM)"
echo " 3. If monitors are wrong, check names with: hyprctl monitors"
echo " and adjust in ~/.config/hypr/hyprland.conf"
echo ""
echo "MONITOR NAMES may differ from X11:"
echo " X11 name -> Wayland name (expected)"
echo " DP-0 -> DP-1"
echo " HDMI-0 -> HDMI-A-1"
echo " DVI-D-0 -> DVI-D-1"
echo ""
echo "KEY BINDINGS (SUPER = Windows key):"
echo " SUPER+Return = Terminal (kitty)"
echo " SUPER+D = Launcher (wofi)"
echo " SUPER+E = File manager (dolphin)"
echo " SUPER+L = Lock screen"
echo " SUPER+Q = Close window"
echo " SUPER+F = Fullscreen"
echo " SUPER+1-9 = Switch workspace"
echo " SUPER+Arrows = Focus monitor"
echo ""
echo "TROUBLESHOOTING:"
echo " - Cursor issues: WLR_NO_HARDWARE_CURSORS=1 is set"
echo " - Blank screen: Check 'nvidia-drm.modeset=1' in kernel params"
echo " - Flickering: Try disabling blur in hyprland.conf"
echo " - Monitor names: Run 'hyprctl monitors' after first launch"
+77
View File
@@ -0,0 +1,77 @@
#!/bin/bash
set -euo pipefail
# saTway Morning Health Check
# Quick Phase 0-2 system health assessment
# Exit codes: 0=healthy, 1=warning, 2=critical
mkdir -p "$HOME/logs" 2>/dev/null || true
LOCAL_LOG="$HOME/logs/health-check-$(date +%Y%m%d).log"
# ANSI colors
RED='\033[0;31m'
YELLOW='\033[0;33m'
GREEN='\033[0;32m'
NC='\033[0m'
WORST_STATE=0
echo "--- Morning Health Check $(date) ---" | tee -a "$LOCAL_LOG"
# Phase 0: Disk usage
echo "[Phase 0] Checking disk usage..." | tee -a "$LOCAL_LOG"
DISK_PCT=$(df / | awk 'NR==2 {gsub(/%/, ""); print $5}')
echo " Root partition: ${DISK_PCT}% used" | tee -a "$LOCAL_LOG"
if [ "$DISK_PCT" -ge 90 ]; then
echo -e " ${RED}CRITICAL: Disk at ${DISK_PCT}%${NC}" | tee -a "$LOCAL_LOG"
WORST_STATE=2
elif [ "$DISK_PCT" -ge 80 ]; then
echo -e " ${YELLOW}WARNING: Disk at ${DISK_PCT}%${NC}" | tee -a "$LOCAL_LOG"
[ "$WORST_STATE" -lt 1 ] && WORST_STATE=1
else
echo -e " ${GREEN}OK${NC}" | tee -a "$LOCAL_LOG"
fi
# Phase 1: Git status
echo "[Phase 1] Checking workspace git status..." | tee -a "$LOCAL_LOG"
WORKSPACE="$HOME/internal/mw-pfeddersheim-workstation"
if [ -d "$WORKSPACE/.git" ]; then
GIT_DIRTY=$(git -C "$WORKSPACE" status --porcelain || true)
if [ -z "$GIT_DIRTY" ]; then
echo -e " ${GREEN}Clean${NC}" | tee -a "$LOCAL_LOG"
else
echo -e " ${YELLOW}Dirty (${#GIT_DIRTY} changes)${NC}" | tee -a "$LOCAL_LOG"
echo "$GIT_DIRTY" | head -5 | tee -a "$LOCAL_LOG"
[ "$WORST_STATE" -lt 1 ] && WORST_STATE=1
fi
else
echo " Workspace not found at $WORKSPACE" | tee -a "$LOCAL_LOG"
fi
# Phase 2: Docker, Uptime & Dependencies
echo "[Phase 2] Checking services & dependencies..." | tee -a "$LOCAL_LOG"
if command -v docker &> /dev/null; then
if docker info &> /dev/null; then
RUNNING=$(docker ps --format "{{.Names}}" 2>/dev/null | wc -l)
echo -e " ${GREEN}Docker: ${RUNNING} container(s) running${NC}" | tee -a "$LOCAL_LOG"
else
echo -e " ${YELLOW}Docker: installed but not running${NC}" | tee -a "$LOCAL_LOG"
[ "$WORST_STATE" -lt 1 ] && WORST_STATE=1
fi
else
echo -e " ${YELLOW}Docker: not installed${NC}" | tee -a "$LOCAL_LOG"
fi
if [ -f "$WORKSPACE/package.json" ]; then
if [ ! -d "$WORKSPACE/node_modules" ]; then
echo -e " ${RED}CRITICAL: node_modules missing. Run 'npm install' or 'pnpm install'${NC}" | tee -a "$LOCAL_LOG"
WORST_STATE=2
else
echo -e " ${GREEN}Dependencies: OK (node_modules present)${NC}" | tee -a "$LOCAL_LOG"
fi
fi
echo " Uptime: $(uptime -p)" | tee -a "$LOCAL_LOG"
echo "--- Health Check Finished $(date) ---" | tee -a "$LOCAL_LOG"
exit "$WORST_STATE"
+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 "============================================="

Some files were not shown because too many files have changed in this diff Show More