- hyprshutdown wired into the graceful power flow (Super+Shift+O logout,
Super+Ctrl+O power off, Super+Ctrl+Shift+O reboot) so apps close cleanly with a
UI before Hyprland exits, instead of a hard systemctl call. Added to the hyprlua
package list.
- Scrolling layout: default window is now 76% of the monitor along the scroll axis
(column_width 0.5 -> 0.76).
- sysupdate.sh: primes sudo once at startup and keeps the credential alive for the
whole run, so no later step re-prompts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUN7gg6GGfnToghMijLm5Y
Replaces the scrolling-preset "columns" with a genuine custom tiling layout
(hl.layout.register "lua:columns"): a row of columns where each column is an
INDEPENDENTLY panning vertical strip. Moving focus up/down inside a column pans
only that column to keep the focus in view; the other columns stay frozen — the
combination-lock behaviour that the built-in scrolling engine (single shared tape
offset) structurally can't do.
- columns.lua: recalculate places every window box; per-workspace state holds each
window's column assignment and each column's pan offset. layout_msg handles the
focus/move/count/orient commands. New windows fill the least-populated column;
changing the column count reflows evenly.
- init.lua: for Lua layouts (lua = true), orientation is sent via layout_msg
("orient h|v") instead of the global scrolling:direction option.
- hypr-nav: forwards focus/move/count for the columns layout to hl.dsp.layout(...);
the built-in scrolling path is unchanged.
- binds.lua: Super+Ctrl+Alt+H/J/K/L manage the column / windows-per-view count
(axes follow the workspace's horizontal/vertical orientation).
Menu dropdown for "Columns" now selects orientation (Horizontal / Vertical).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUN7gg6GGfnToghMijLm5Y
astal-menu: the taskbar ⤢ toggle now collapses the compact strip and mounts
its workspace/window panel as a takeover overlay covering the 2x2 quads (with
a Back button), instead of a popover/inline reveal. QuadGrid gains a generic
takeover overlay; the menu window coordinates show/hide and resets on close.
hypr: new "Columns" layout (scrolling backend presented as independent columns)
with a menu dropdown to pick orientation — "Left / Right" or "Up / Down" — via a
new dir_labels manifest field. The layout menu now tracks direction dropdowns and
fit switches per-layout, so a second directional layout no longer clobbers the
first.
binds/scripts: focus & window-move keys (hjkl + arrows) route through a new
hypr-nav helper so they stay screen-absolute in the scrolling/columns layouts
(the layout otherwise resolves a perpendicular direction onto "scroll the tape",
rotating the keys with scrolling:direction). Adds explicit cross-monitor binds:
Super+,/. focus prev/next monitor, Super+Shift+,/. move the window there.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUN7gg6GGfnToghMijLm5Y
Replace the layout dropdown with a tab bar (one tab per layout — Dwindle · Master ·
Monocle · Scrolling, from the manifest) over a Gtk.Stack of per-layout options that
switches with the tab. Selecting a tab applies that layout to the currently focused
workspace; the scrolling page carries its options — a "direction" dropdown and a
"center-focused" switch (focus_fit_method). The active tab reflects the workspace's
real (per-ws) layout via the layouts state file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
- hypr/layouts/: drop-in layout registry. init.lua auto-discovers layouts/<name>.lua
(each self-describing) and exposes the global `layouts` table: set_default (picked
in hyprland.lua — now scrolling, direction down), set(ws,name,dir) for live
per-workspace switching (verified it re-tiles the active ws), set_fit(0/1). Writes
a manifest + per-ws state for the menu. Ships scrolling/dwindle/master/monocle.
- Scrolling default: column_width 0.5 (active column < viewport so prev/next stay
clickable), focus_fit_method 1 (centered follow), fullscreen_on_one_column.
- astal-menu taskbar pop-open: workspace layout selector + direction + a "Centered"
toggle (focus_fit_method), and a per-window list — [icon+title → jump][⇤ here →
pull to current workspace], keeping jump-to-window intact.
- eww bar menu buttons open the menu explicitly from the top (menu-toggle.sh toggle top).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
- Installer: add python-pillow (map tile stitching) and networkmanager (the Network
quad's nmcli backend) so a fresh install has a working map and network panel.
- Super+D binds explicitly to `menu-toggle.sh toggle top` so it pops in from the top.
The astal-menu config (incl. new backend/nm.py, ui/statsbar.py, etc.) already ships
via the config-updater CONFIGS list and is re-synced by sysupdate.sh; settings live
in XDG_STATE_HOME so updates don't wipe them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
- Add a minimalist system-stats line at the top of the panel: CPU / MEM / GPU / disk
utilisation + network down/up rate, read straight from /proc and /sys on a 2s timer
(no subprocess). ui/statsbar.py.
- Opening side is selectable: `main.py --side top|bottom|left|right` (also via
`menu-toggle.sh <verb> <side>` or just `menu-toggle.sh <side>`). The panel anchors
flush to that edge, and the compositor's layer slide brings it in from there.
Unified all layer anchoring into MenuWindow._apply_anchor.
- App drawer now does a bouncy pop-expand (CSS scale, play-once guard like the quads).
- Tighten the action-pill buttons vertically (padding 6->1px, min-height 32->22) so
they stop wasting vertical space.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
The Location map was a fixed static image with no way to move the viewport, on the
light OSM tiles. Now:
- Drag to pan (re-renders at the new centre; the pin stays anchored to the real
location and clamps to the edge when panned away), scroll to zoom, double-click to
recentre. Panning/zooming coalesce into ~11 renders/sec.
- Tiles default to the dark CartoDB "night" map (fits the theme); a new "Satellite
view" feature toggle switches to Esri World Imagery. staticmap.py gained a style
arg + a separate marker coordinate, and caches tiles per style.
Verified: dark + satellite render, drag re-centres the view (pin goes off-centre),
and the coordinate round-trip is exact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
- Connect flow now shows real status: a spinner + Cancel while connecting, and a red
"failed" tag + Retry if the attempt stops without linking (via notify::connecting
going false) or a 25s timeout backstop fires. Previously Connect gave no feedback
and could look stuck forever.
- Add a Forget button on paired devices (adapter.remove_device) to unpair them.
- Track per-device pending/failed state so the row reflects connecting/failed/
connected/paired accurately.
Verified the paired row renders Forget + Connect and the connected row renders
Forget + Disconnect.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
The device rows already render a "Disconnect" button when a device is connected,
but the list only rebuilt on adapter-level notify::devices / notify::is-powered —
not on per-device state changes. So a device connected (via the menu or elsewhere)
while the menu was open kept its "Connect" button and offered no way to disconnect.
Hook each device's notify::connected/connecting/paired (once each) to refresh the
list, so a connected device flips to "Disconnect".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
Switches sat with the default FILL vertical alignment, so in taller rows (e.g. the
Bluetooth Power switch in the expanded card's header) they stretched to the row
height and rendered as a squared/vertical blob. Give every Gtk.Switch
valign=CENTER so it keeps its fixed pill height regardless of the row.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
The .quad-pop CSS animation stayed applied for the whole time a quad was expanded,
so every re-layout of the expanded card (module content streaming in, the compact
grid updating beneath the overlay) restarted the transform animation — making the
pop repeat continuously. Remove the class ~340ms after expanding (just past the
300ms animation) so it plays exactly once and no later re-layout can retrigger it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
- Main open: enable Hyprland's `layers` animation with the bouncy myBezier curve and
a slide style, so the layer-shell control centre slides in from the top edge with a
little overshoot instead of just appearing. (Also gives notifications/bar the same
springy slide, which suits the DE.) A per-namespace layerrule was tried first but
this hyprlua build doesn't honour its animation timing.
- Quad expand: a CSS @keyframes scale-bounce (scale 0.82 -> 1.05 -> 1.0) added to the
expanded card on expand and cleared on collapse so it replays every time. Verified
GTK4 CSS transforms/keyframes render on this build, and that the card scales during
the animation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
Favourites:
- Pin/unpin from the drawer never worked: the right-click / long-press gestures
were added to a Gtk.Button in the default (bubble) phase, where the button's own
primary-click gesture swallowed them. Move them to the CAPTURE phase and claim the
sequence (shared add_pin_gestures helper) so they fire reliably and don't also
launch the app.
- Show a ★ on pinned tiles and re-render tiles when favourites change, so the state
is visible and updates live.
- Verified end-to-end (right-click pins, right-click again unpins).
Weather: the expanded quad showed current conditions only. weather.sh used
`${2:-0}`, which rewrote the expanded view's intentionally-empty opts to "0"
(current only); use `${2-0}` so the empty value reaches wttr.in as its default
3-day forecast. Verified the expanded view now renders all three days.
Settings: move settings.json from CONFIG_DIR to XDG_STATE_HOME. The config-updater
`rm -rf`s ~/.config/astal-menu on every deploy, which was wiping pinned favourites
and quad toggles; the state dir is untouched by config sync. Migrates the old file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
- Switches: give them a fixed pill with a bordered round knob (the reset theme
left them shapeless).
- Close (x) button: inset with a margin so it no longer overlaps the drawn border.
- Height: the panel overflowed the monitor. Trim the map compact height, quad
min-height, card padding, grid/panel spacing, top margin, and drawer strip so it
fits (≈1600 -> ≈1310 logical on a 1440 display).
- App drawer expand button: use the same nf-fa-expand/compress glyphs as the module
expand buttons instead of chevrons, so all expand controls look uniform.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
Network expanded view is rewritten into four tabs over a new nmcli backend
(backend/nm.py, JSON in/out):
- Adapters: each device foldout with editable IPv4 and IPv6 — Automatic/DHCP
toggle, address, a linked subnet-mask <-> CIDR pair (edit either, the other
follows), gateway; applied via `nmcli connection modify/up`. Manual fields dim
when DHCP is on.
- Routes: the kernel routing table (read-only; editing kernel routes needs root).
- VLAN: pick a parent device + ID to create/bring up a VLAN, list + activate/delete.
- DNS: effective servers per device + a per-connection override (servers +
ignore-automatic).
Verified reads and the write path end-to-end (VLAN add/delete via nmcli; each tab
rendered in a harness).
Also: flatten the settings-cog MenuButton's inner `button` node so it shows one
ring instead of two, and drop the border on the close (x) button.
Weather keeps its wttr.in 3-day forecast (unchanged, per request).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
- Borders: with the app stylesheet now above the theme, the CSS `.quad-card`
border rendered a second ring concentric with the Cairo bordered() ring. Drop the
CSS border/background/radius from the card boxes so only the Cairo card shows.
- Bluetooth: the power/scan/connect controls were correctly wired to AstalBluetooth,
but the adapter was rfkill soft-blocked, making set_powered a silent no-op. The
power toggle now runs `rfkill unblock bluetooth` before powering on.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
The panel showed a solid #1a1a1a slab between modules. Root cause: the CyberQueer
GTK theme is installed as ~/.config/gtk-4.0/gtk.css (a symlink), which GTK4 loads
at PRIORITY_USER (800) — above our PRIORITY_APPLICATION (600) — and its reset rule
`* { background-color: #1a1a1a }` painted every node, including the structural
containers between modules. Our transparency overrides silently lost the cascade.
Fixes:
- theme.py: load the app stylesheets at USER+1 so they sit just above the
user-level theme symlink and actually win.
- style.css: blank the structural container nodes (window/.panel/overlay/revealer/
grid/scrolledwindow/viewport/drawingarea) so the desktop shows between modules.
- window.py/quadgrid.py: draw each module's card background with the Cairo
bordered(fill_bg=True) so cards stay opaque (incl. rounded corners) while the
gaps go transparent — this GTK build doesn't paint CSS backgrounds on containers,
hence the Cairo fill.
Popovers/buttons keep their backgrounds (the theme's rule still covers those nodes;
only structural containers are overridden). Verified visually end-to-end.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
The Open-windows taskbar dispatched `hyprctl dispatch focuswindow address:…`,
but this is a hyprlua setup where `hyprctl dispatch` evaluates its argument as
Lua — so the native dispatcher syntax raised a Lua parse error and nothing
happened. Switch to `hl.dsp.focus({ window = "address:…" })`, the hyprlua focus
dispatcher, which focuses the window and switches to its workspace. Verified it
moves across workspaces end-to-end.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
astal-menu-start.sh LD_PRELOADs libgtk4-layer-shell so it loads before
libwayland-client. That preload is only needed at the daemon's own exec — the
library is resident afterwards and the variable is never re-read — but every app
started via AstalApps.launch() inherited it, and Firefox aborts at startup with
libgtk4-layer-shell preloaded (gdk_display_manager_get() before gtk_init()).
main.py now drops LD_PRELOAD from its environment right after startup, so
launched GUI apps (and backend subprocesses) run with a clean env. Verified the
layer-shell library stays mapped and the menu surface still maps afterwards.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
Widget settings popover toggles now take effect immediately. QuadCard only
rebuilt a card when its Enabled state flipped, so per-module feature toggles
(Wi-Fi, discovery, routes, CLI art, …) were persisted but never applied. The
card now rebuilds whenever any of its own feature values change, and the
incomplete partial per-module refresh callbacks (which also leaked a dead
subscription per rebuild) are dropped in favour of that single path.
Make the backmost surface transparent so each module floats on its own drawn
border/background: force `window`/`.background`/`.panel` transparent, since a
plain `.menu-window` rule did not override the GTK theme's solid window node.
Locate via IP now works and uses traceroute: geolocate.py traceroutes to
1.1.1.1, takes the first globally-routable hop (the ISP egress) and resolves it
through a public geolocation API, falling back to self-IP when traceroute is
missing or finds no public hop. The ip_locate toggle gates the lookup entirely
(placeholder when off). Adds `traceroute` to the hyprlua package list.
Autostart: `sleep 1 && hyprctl reload` after all spawns so layer-shell clients
settle on the final config.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
A touch-friendly GTK4 popup control centre (Python + PyGObject, wlr-layer-shell)
launched from the EWW bar or Super+D. Replaces nwg-dock and nwg-drawer.
Layout: a floating, drawn-bordered panel (anchored top-centre so it never blocks
the rest of the screen) with a 2x2 quad grid over a full-width app drawer, plus a
top taskbar. Any quad expands over the others; the drawer expands to the bottom.
Modules:
- Location — static OSM map (backend/staticmap.py) on IP geolocation
(libshumate won't paint tiles in this env; shumate-demo is blank too)
- Weather — wttr.in ANSI art rendered via an SGR parser into a TextView
- Bluetooth — AstalBluetooth: discovery/connect/disconnect + local history
- Network — AstalNetwork wifi + nmcli/ip/ss for IP, DHCP/manual, routes, ports,
public IP; per-feature toggles
- Taskbar — open windows from hyprctl, grouped by app with pop-out per instance
- Favorites — pinned apps (right-click/long-press to pin) atop the app drawer
Notes:
- Frontend is Python/GTK4, not Lua: lgi/Astal-Lua are GTK3-only.
- Module borders are drawn with a Cairo overlay (lib/border.py); this GTK build's
renderer skips CSS border/background on plain container widgets.
- Consumes Astal GObject libs (io/apps/network/bluetooth) via introspection.
Wiring: autostart + Super+D/Super+Shift+A binds, EWW launcher button in all three
bar variants, apply-theme.sh (_colors.css), installer packages, config-updater.
drawer.sh moved to scripts/deprecated/.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The dialog was landing off-cursor and at ~half-screen. Two Hyprland
gotchas were fighting each other: `move` is evaluated against the
window's natural size and `size` is applied afterwards from the top-left,
so a cursor_x-(window_w/2) move centred the pre-shrink window. zenity's
file chooser also ignores --width/--height and has a ~738x363 GTK
minimum.
Fix the size at 760x460 and hard-code the move offsets to half that
(380x230) so the shrunk window lands exactly on the pointer. Drop the
dead zenity --width/--height sizing from the script.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a size rule alongside the move so the zenity dialog no longer opens
at its default ~50% size. zenity clamps to its GTK minimum (~738x363),
which is well under half the screen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rename the at-cursor rule to filepicker and move the dialog to a fixed
position 10% into the display (from the top-left) instead of following
the cursor.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a dedicated at-cursor window rule (float + move to cursor) and point
the Super+F / Super+Shift+F filepicker binds at it, so the zenity dialog
opens centered on the mouse instead of mid-screen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Prefix the Super+F / Super+Shift+F filepicker binds with [tag +centered]
so the zenity dialog spawns as a centered floating window (60% x 60%),
matching the centered rule in windowrules.lua. Same tagged-spawn
mechanism as Super+U.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add filepicker-clipboard.sh: a zenity file dialog that copies the
selected file's path (Super+F) or contents (Super+Shift+F) to the
clipboard via wl-copy. Path mode collapses $HOME to ~.
Repoints Super+F / Super+Shift+F from wofi-file-search/foldersearch
(kept as comments) in usr/binds.lua.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a self-contained CyberQueer login skin for the greetd/ReGreet greeter and
make it a first-class, selectable module.
Theme:
- New etc-greetd/regreet.css: login-specific stylesheet (glowing pink clock +
card, violet-bordered combos, hot-pink Login, quiet red-outline power
buttons). Loaded by ReGreet at APPLICATION priority over an Adwaita-dark base;
regreet.toml theme_name switched cyberqueer->Adwaita to avoid the desktop
theme's `* { all: unset }`. Redefines Adwaita's accent/destructive named
colours so the semantic buttons recolour. Verified via headless demo render.
- Register regreet.css in apply-theme.sh SYS_FILES so palette edits propagate.
Installers:
- modules.conf: add greetd-regreet (three-way mutually exclusive with lightdm+ly).
- Regenerate tui-install.sh, generate-answerfile.sh, docs/md/modules.md via
generate-modules.sh.
- install-modules.sh (hand-maintained): add greetd-regreet to counter/menu/
summary/dispatch.
Migration:
- New migrate-to-greetd.sh: migrate to Plymouth + greetd/ReGreet (greetd is the
target, so it's not in the disable list). migrate-to-lightdm.sh relabelled the
Xorg alternative.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1ApxGuM3N3nSiAJonSjMQ
Rework the M-Archy splash: scale the skull to ~42% of screen height and
centre it instead of filling the screen, then draw the latest 3 boot-log
lines beneath it (fed by both the status and display-message callbacks).
Drops the imagemagick-generated dot.png throbber and its refresh loop.
Render the logo at 600px (down from 1920px) since it is no longer
full-screen, and only pull in imagemagick for the transparent-placeholder
fallback. migrate-to-lightdm purges the stale dot.png before re-running the
module so upgraded installs end up byte-clean. Update docs and module
labels ("skull logo + boot-log tail"), and simplify bg-skull.svg.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the Xorg lightdm-gtk-greeter with a wlroots-native greetd + ReGreet
greeter (inside a cage kiosk), fixing three issues rooted in running an X
greeter on a multi-monitor / HiDPI / Wayland box: a blank output, tiny
scaling, and the Xorg->Wayland VT/seat handover race that left the Hyprland
session with dead keyboard/mouse.
- etc-greetd/: greetd config, ReGreet config (cyberqueer GTK4 theme, larger
font, wallpaper background), cage session launcher, and a password + FIDO
PAM stack (no securetty/OATH, which a graphical greeter cannot drive).
- greetd-regreet.sh: install/enable module, mutually exclusive with the
lightdm and ly greeter modules.
- Greeter background tracks the desktop wallpaper: a root systemd .path unit
mirrors the picker's choice to a greeter-readable path (the user's home is
0700, so the unprivileged greeter cannot read it directly).
The LightDM path is kept as a dormant alternative and hardened en route:
pam_u2f (FIDO) added to its PAM stack, a HiDPI + multi-monitor
display-setup.sh, a larger greeter font, and the same wallpaper-sync units.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plymouth: bg-skull.svg is a 1920x1080 transparent composition but was rendered
at 300px and treated as a small centred logo, so the skull all but vanished and
the spinner (positioned off the logo height) landed on the monitor artwork. Now
render at native 1920px, scale to fill the screen, and pin the spinner at a
fixed 0.93 of screen height. Also fix DOTFILES_DIR (was off by one level, so the
bundled SVG never resolved and it fell back to a transparent placeholder) and
the dead ../plymouth.sh paths in plymouth-custom.sh and install-modules.sh.
Greeter: HyprLua now uses LightDM + lightdm-gtk-greeter, themed with the
existing cyberqueer GTK theme and a rendered skull background, via a new
self-contained lightdm module. ly is extracted from the DE scripts into its own
module so it stays installable; niri and legacy hyprland keep their inline ly.
Both greeters are registered in modules.conf as mutually exclusive, and the
generated installer/answerfile/docs regions were rebuilt from it. install-modules.sh
guards the 'ly' token with a padded match so it can't substring-hit 'p(ly)mouth'.
Add setup/tools/migrate-to-lightdm.sh to move an existing install (ly / greetd /
GDM / SDDM / LXDM) onto the Plymouth + LightDM setup: it disables the old DM for
the next boot only (never stops the running session) and delegates install +
theming to the module scripts. Supports --dry-run / --no-plymouth / --yes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The first checklist (Select Components) could silently auto-confirm its
defaults, swallowing the user's toggle: tui-install.sh runs right after the
long, mostly-unattended base install, and keystrokes buffered during that
wait were consumed by the script's very first read. Drain the tty before the
first prompt.
Also split the "shell" component into "shell" (tools/plugins/programs) and
a new "shellrc" (personal .bashrc/.zshrc/.vimrc), so the shell environment
can be installed without overwriting the user's rc files. shell-setup.sh
reads MARCHY_DEPLOY_SHELL_RC to gate the rc symlinks and now also deploys
.vimrc, which /etc/skel sync already expected but nothing ever created.
Removes the separate, easily-desynced "Shell Config for New Users" prompt
in favor of deriving /etc/skel behavior from the same shellrc choice, and
mirrors the removal in generate-answerfile.sh.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New docs/md/ansipa-administration.md — an operations-focused guide, separate
from the architecture doc (freeipa-ansible.md) and the deployment guide
(ansipa-setup.md). Covers:
- How administration works: group-driven, the policy enforcer vs the software
installer timers, host- vs user-group scope, immediate-apply commands, and
the everyday ipa hostgroup/group commands.
- The full policy catalogue with per-policy apply/revert behaviour, targeting
(name suffix vs group description), and prerequisites:
device (dev_daemon-*, dev_timeshift-backup, dev_no-local-users,
dev_local-sudo-*, dev_ssh_*), monitoring (dev_mon_base/clamscan/timeshift/
power, usr_mon_logins), software installers (dev_pkg_*, dev_mod_*, dev_fp_*),
user (usr_admin, usr_block-binary-*, usr_prt_*, usr_smb_*), and BaseUser sync.
- Common recipes (onboard a workstation, grant/block, live policy editing) and
a troubleshooting table.
Linked from the docs index.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUhrcFU8J1Hnf7vNqNxZNi
Rename the security-scan policy to dev_mon_clamscan (fits the XXX_mon_* naming
for monitoring groups) and make it hand results to CheckMK natively instead of
uploading logs to an SMB share.
- dev_mon_clamscan.sh: runs the daily ClamAV/rkhunter/chkrootkit scan and writes
its own CheckMK local check (ansipa_clamscan → service "Ansipa_ClamScan"); the
agent from dev_mon_base collects it. Removes the SMB upload entirely. Discovery
matches dev_mon_clamscan BEFORE the generic dev_mon_* catch-all so it sets
WANT_CLAMSCAN rather than being treated as a dev_mon.sh check. mkdir -p the
cron dir (minimal hosts lack /etc/cron.d until cron is installed).
- Remove the now-redundant dev_mon_malware check from dev_mon.sh (subsumed).
- Remove usr_scan-notify and the whole SMB scan-alert pipeline it fed:
ansipa-fetch-alerts.sh, ansipa-scan-notify.sh, server ansipa-check-scans.sh,
the ansipa-scans Samba share + scanupload account, /etc/ansipa-smb.creds, and
the SMB_SCAN_PASSWORD/smb_scan_password variables (playbook, compose, run.sh,
.env, Dockerfile). The ansipa-luks-keys and ansipa-policystore shares stay.
Verified live on the Arch client: enrolling in dev_mon_clamscan writes the scan
script, cron, and local check (which reports "Ansipa_ClamScan"); leaving the
group removes all three. Docs updated (policy tables, architecture, SMB shares,
scan-flow → CheckMK).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUhrcFU8J1Hnf7vNqNxZNi
Both containers were reporting "unhealthy" while serving fine — the
healthchecks I added probed the wrong address/endpoint:
- nginx: `wget localhost` resolves to IPv6 ::1 inside the container, but nginx
listens on IPv4 0.0.0.0:80 only, so the probe was refused. Use 127.0.0.1.
- keycloak: with KC_HTTP_RELATIVE_PATH=/auth, /health/ready on the 9000
management port returns 404. Probe /auth/realms/master on the serving port
8080 instead (bash /dev/tcp — the image has no curl/wget).
Verified all five containers report healthy and the gateway serves /, /ipa/ui/,
/cmk/ and /auth/ correctly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUhrcFU8J1Hnf7vNqNxZNi
- ansipa Setup Guide: add a Firewall section — which inbound ports to open on
the server (minimum LDAP/Kerberos/HTTPS + per-feature SMB/CheckMK/Keycloak/
gateway), that clients need nothing inbound for policy enforcement or CheckMK
push mode, and copy-paste firewalld / ufw / nftables rule examples.
- freeipa-ansible.md: reflect the now-supported mixed fleet (Arch, Fedora,
RHEL/Rocky/Alma, Debian/Ubuntu) instead of "Arch-only", note the Arch
autofs/AUR enrollment caveat, and cross-link the setup guide.
Nothing about the deployment is left to guesswork: architecture, every port,
firewall rules, the nginx portal + reverse-proxy snippets, NAT/push-mode, the
Proxmox LXC generator, and the tools reorg paths are all documented.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUhrcFU8J1Hnf7vNqNxZNi
niri contradictorily enabled BOTH ly@tty1 and greetd.service (it deployed a
greetd-tuigreet config and enabled greetd late in the script) while also
configuring and enabling ly — so two display managers raced for tty1 and the
stale greetd/tuigreet greeter could win. niri's intent is ly (it disables
getty@tty1, deploys ly config, enables ly@tty1).
Add disable_service greetd.service next to the ly enable and remove the
greetd-config-deploy-and-enable block, matching the hyprlua/hyprland fix. ly
lists the niri Wayland session from /usr/share/wayland-sessions, so no greetd
path is needed. Verified the same fix live on a hyprlua install (greetd
disabled, ly@tty1 enabled).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUhrcFU8J1Hnf7vNqNxZNi
Generate everything needed to run the complete ansipa stack (FreeIPA + CheckMK
+ Keycloak + PostgreSQL + nginx gateway) inside a single Proxmox LXC container.
Unlike freeipa-image.sh's proxmox-lxc target (which exports a FreeIPA-only CT
rootfs), this runs the tested docker-compose stack inside a privileged,
nesting-enabled CT ("Docker-in-LXC"), so it is the whole management plane in
one container.
Prompts for domain/realm/VMID/resources, generates all service secrets, and
emits: pct-create-<vmid>.sh, ansipa-<vmid>.conf (nesting=1,keyctl=1,fuse=1 +
the cgroup/apparmor allowances FreeIPA's systemd and Docker overlay2 need), a
first-boot provisioner that installs Docker and brings the stack up via run.sh,
a filled-in .env (mode 600), and a DEPLOY guide. Verified the generator emits
correct, syntactically-valid artifacts. Documented in the ansipa Setup Guide.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUhrcFU8J1Hnf7vNqNxZNi
Move the build and deployment scripts into setup/tools alongside the existing
generators (proxmox-lxc-gen.sh, freeipa-image.sh, generate-modules.sh):
- setup/generate-answerfile.sh -> setup/tools/generate-answerfile.sh
- setup/archiso/build.sh -> setup/tools/build.sh
- setup/archiso/wds-deploy.sh -> setup/tools/wds-deploy.sh
- setup/archiso/write-usb.sh -> setup/tools/write-usb.sh
The archiso overlay/ (profile data: airootfs, profiledef.sh, packages.extra)
stays in setup/archiso/; build.sh now resolves it via an OVERLAY_DIR anchored
at the repo root, and DOTFILES_DIR is corrected for the new two-levels-deep
location. Updated every reference (generate-modules.sh, readme.md,
docs/md/{archiso,installation,index}.md).
generate-answerfile.sh: add linux-hardened to the kernel menu; verified its
emitted answerfile schema still matches every field the installers consume
(drive/kernel/keymap/hostname/username/password/luks_password/encrypt/
fido2_*/run_tui/components/desktop_environment/apps/shell_rc/colors).
Docs: add a dedicated ansipa Setup Guide (docs/md/ansipa-setup.md) covering
architecture, the full port list, the nginx portal + reverse-proxy paths, the
exact upstream reverse-proxy snippets (nginx/Caddy), NAT/push-mode behaviour,
and the Arch autofs/AUR caveat; link it from the index and cross-reference the
CheckMK/nginx ports from freeipa-ansible.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUhrcFU8J1Hnf7vNqNxZNi
Statically audited every optional-module and DE script's package tokens
against the official repos, the AUR, and Flathub. Ten modules named a package
that does not exist at the source they used, so each would abort with "target
not found" (pacman), an AUR miss, or a Flathub 404:
Installed via pacman but AUR-only (switched to the yay AUR helper):
- tetris: bastet
- doom: chocolate-doom, freedoom
- openarena:openarena
- openshot: openshot
- freeipa-client (module): oddjob — now optional via yay with a
pam_mkhomedir fallback (matches freeipa-enroll.sh)
Wrong AUR/repo package names:
- cecilia: cecilia -> cecilia-git
- productivity: python-jrnl -> jrnl (in the official repos)
- localtunnel: AUR fallback localtunnel -> nodejs-localtunnel
Wrong/absent Flatpak IDs:
- zen-browser: io.github.zen_browser.zen -> app.zen_browser.zen
- stuntrally: io.github.stuntrally.StuntRally3 is not on Flathub at all;
switch to the AUR stuntrally-bin (the actual Stunt Rally 3)
Verified all corrected names resolve (repo / AUR RPC / Flathub API). Modules
whose tokens are in the official repos and merely installed via yay (nyxt,
toot, llama-cpp — yay handles both sources) were left as-is.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUhrcFU8J1Hnf7vNqNxZNi
Add an nginx service that fronts all three web UIs on a single hostname/port
and serves a portal landing page, designed to sit behind the operator's own
TLS-terminating reverse proxy.
- Each backend is proxied at the path it already serves natively — FreeIPA
/ipa, CheckMK /cmk, Keycloak under a configured /auth relative path — so no
fragile path rewriting is needed and one upstream proxy line exposes
everything. FreeIPA's strict Host/Referer anti-CSRF checks are satisfied by
pinning Host to the IPA hostname and rewriting Referer.
- Honors incoming X-Forwarded-Proto/For so it works behind a second reverse
proxy (double-proxy verified: portal, FreeIPA, CheckMK and Keycloak all
reachable through two layers, and a CheckMK login POST completes to an
authenticated session through the gateway).
- Keycloak: KC_HTTP_RELATIVE_PATH=/auth + KC_PROXY_HEADERS=xforwarded so it
builds correct URLs behind the gateway; deliberately not forwarding
X-Forwarded-Port (this gateway is :80 internally; the public port is the
upstream proxy's) to avoid https://host:80 redirects.
- .env.example documents ANSIPA_HTTP_PORT and gives the exact upstream
reverse-proxy snippet (nginx location block + Caddy one-liner).
Portal is a static cyberqueer-themed page linking to /ipa/ui/, /cmk/, /auth/.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUhrcFU8J1Hnf7vNqNxZNi
Found while running the policy matrix and NAT/WAN scenario in VMs:
- ansipa-enforce-policies.sh and the dev_security-scan generated script called
`hostname -f`, but the hostname binary (inetutils) is not installed on a
minimal Arch system, so the enforcer aborted with "hostname: command not
found" on every run. Add an ansipa_fqdn() helper (hostnamectl / getent /
/etc/hostname / kernel-hostname fallback chain) and use it; inline the same
fallback in the standalone scan script.
- Fix two stacked bugs that meant CheckMK push mode — the only monitoring
transport that works when a client is behind NAT — never functioned:
* ansipa-checkmk-setup.sh referenced IPA_ADMIN_PASS in the DONE_FLAG
early-exit path, but that variable was assigned only afterwards, so every
rerun kinit'd with an empty password, failed silently, and skipped the
push-mode request writer. Resolve config before the early exit.
* _write_push_requests parsed `ipa hostgroup-show dev_mon_base` members with
awk that did `next` on the "Member hosts:" line — but ipa prints members on
that same line, so the only line with hostnames was discarded and no push
request was ever written. Rewrite the awk to capture the label line's value
plus wrapped continuation lines. Verified end-to-end: requests are written
and CheckMK flips the host to cmk_agent_connection=push-agent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUhrcFU8J1Hnf7vNqNxZNi
core.sh enables greetd (tuigreet) on every install, but the ly-based Hyprland
DEs only disabled getty@tty1 and enabled ly@tty1 — leaving both display
managers enabled and racing for tty1, so the stale greetd/tuigreet greeter
could win and show the old text login instead of ly.
Add a disable_service helper (mirrors enable_service; best-effort, non-fatal)
and call disable_service greetd.service in hyprlua.sh and hyprland.sh right
before enabling ly@tty1, so ly owns the login screen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUhrcFU8J1Hnf7vNqNxZNi
Found while bringing up the FreeIPA+CheckMK+Keycloak stack in a VM:
- Pin volume and network names (name:) so compose and the cgroup-v2 run.sh
wrapper share the same objects. Compose otherwise prefixes the project name
(image_cmk-creds, image_ipa-net), so the run.sh-started freeipa container
mounted a different cmk-creds volume than the compose-started checkmk wrote
to — the automation secret never reached IPA and the dev_mon_* integration
retried forever. run.sh also now labels the network it creates so compose
adopts it instead of erroring "exists but not created by compose".
- run.sh: mount cmk-creds into freeipa and include checkmk in the "all" target;
both were missing, so the CheckMK path was never wired up via run.sh.
- Fix both healthchecks: Keycloak has no curl/wget and serves /health on the
management port 9000 only when KC_HEALTH_ENABLED=true, so the old curl probe
left it permanently unhealthy — use a bash /dev/tcp probe against 9000.
CheckMK's /api/1.0/version needs auth (401), so probe the login page instead.
- Advertise a client-routable CheckMK URL (CMK_ADVERTISED_URL) in the
dev_mon_base description instead of the container-internal 172.30.0.12, which
enrolled clients cannot reach; falls back to the internal URL when unset.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUhrcFU8J1Hnf7vNqNxZNi