Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a69cacc006 |
No files matched your search
@@ -183,8 +183,8 @@ currently parked (dir present, unit not active) — imported anyway, noted as
|
||||
parked in the catalog.
|
||||
|
||||
**Excluded:** Watchtower (retired — replaced by the update timer after it took
|
||||
gitea down; its dir stays on junior until cleanup), the app source
|
||||
checkouts such as sierraandtyler and Bang (deployed by CI from their own repos —
|
||||
gitea down; its dir stays on junior until cleanup), Completeuphoria,
|
||||
sierraandtyler, Bang (source checkouts deployed by CI from their own repos —
|
||||
Panama owning a second copy of their compose would drift), `ci/`, `logs/`,
|
||||
and junior's loose scripts other than `update-containers.sh` (§4).
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/panama
|
||||
@@ -9,7 +9,7 @@ You are editing a running desktop, not a codebase that gets deployed later. Ever
|
||||
repository is symlinked into `~/.config`, so a save is live the moment it lands. That single fact
|
||||
drives every rule below.
|
||||
|
||||
Before large work, read these in order:
|
||||
Read before large work, in this order:
|
||||
|
||||
- `README.md` — layout, the `panama` command, how installing and updating work
|
||||
- `config/dot/hypr/README.md` — the compositor config is **Lua, not hyprlang**; read "The one
|
||||
@@ -62,8 +62,7 @@ Every executable contract under `tests/` is classified in `tests/contracts.manif
|
||||
`PANAMA_TEST_TIMEOUT_SECONDS` value. Failures print the contract's captured stdout and stderr.
|
||||
Successful stdout stays quiet. Successful stderr is surfaced as a warning.
|
||||
- Contracts run directly too: `tests/setup/interview-contract`.
|
||||
- After changing `PreferenceSchema.qml` or
|
||||
`config/dot/quickshell/services/SettingsRoutes.qml`, regenerate:
|
||||
- After changing `PreferenceSchema.qml` or `services/SettingsRoutes.qml`, regenerate:
|
||||
`config/dot/quickshell/scripts/panama-settings-docs` (writes `docs/settings.md`) and
|
||||
`config/dot/quickshell/scripts/panama-settings-commands` (writes the launcher deep links).
|
||||
Both take `--check`; `tests/quickshell/settings-docs-contract` fails when stale.
|
||||
+5
-11
@@ -3,21 +3,15 @@
|
||||
/config/bash/.env.lock
|
||||
# Server secrets and state never enter the repo: live .env files belong in
|
||||
# ~/Server/<Name>/ (only compose.yml is symlinked back here), and data/ is
|
||||
# the bind-mount convention (the home server's older services use volume/ and
|
||||
# volumes/). Ignored anyway as a seatbelt -- the repo is public, and
|
||||
# tests/server/compose-secrets-contract is the check that these lines still exist.
|
||||
/server/**/.env
|
||||
/server/**/data/
|
||||
/server/**/volume/
|
||||
/server/**/volumes/
|
||||
# the bind-mount convention. Ignored anyway as a seatbelt -- the repo is
|
||||
# public, and tests/server/compose-secrets-contract is the check that this
|
||||
# line still exists.
|
||||
/server/containers/**/.env
|
||||
/server/containers/**/data/
|
||||
# Personal espanso triggers (name, email), seeded per-machine by setup-identity.
|
||||
/config/dot/espanso/match/identity.yml
|
||||
# Ignore backups of old config files
|
||||
/config/old
|
||||
# MCP bearer tokens. The server list in user/agents/mcp/servers is tracked
|
||||
# and names which variable carries each token; the tokens themselves are
|
||||
# machine-local, because this repo is public.
|
||||
/user/agents/mcp/env
|
||||
# Ignore Wireguard config of course!
|
||||
/config/wg/**
|
||||
# Ignore Neovim lazy-lock file
|
||||
|
||||
@@ -102,13 +102,13 @@ in order, without stopping again:
|
||||
| `interview` | Every prompt, before anything is installed. Answers last one run and are never written to a durable path |
|
||||
| `install-packages` | Repos (RPM Fusion, Terra, Hyprland COPR), the package lists in `setup/packages/`, then whichever optional categories were chosen |
|
||||
| `link-dotfiles` | Symlinks `config/dot/<name>` → `~/.config/<name>`, and seeds the wallpaper, cursor theme and Firefox chrome |
|
||||
| `link-skills` | Links the agent skills in `skills/` into `~/.agents/skills` and `~/.claude/skills`, one per skill. Every machine gets these; personal ones link after and win a name clash |
|
||||
| `link-skills` | Links the agent skills in `skills/` into `~/.claude/skills`, one per skill. Every machine gets these; personal ones link after and win a name clash |
|
||||
| `link-user` | Links the personal content in `user/` — agent instructions, SSH host aliases — but only on a machine that answered yes. See [user/README.md](user/README.md) |
|
||||
| `change-settings` | Copies `config/copy/` over `/`, applies gsettings, enables user services |
|
||||
| `link-vicinae-scripts` | Publishes the Vicinae script commands |
|
||||
| `setup-identity` | git config, `gh auth login`, an SSH key — whichever were asked for |
|
||||
| `install-hardware` | NVIDIA, Secure Boot enrollment, Fedora's extras, firmware — each only if it was asked for. Last, because enrollment and firmware are consumed at the next boot |
|
||||
| `setup-server` | Server role only: linger, unprivileged ports from 80, firewalld (80/443/81, opened only where nothing already grants them), the `nginx-bridge` network, and the nightly image-update timer |
|
||||
| `setup-server` | Server role only: linger, unprivileged ports from 80, firewalld (80/443/81), the `nginx-bridge` network, and the nightly image-update timer |
|
||||
| `link-server` | Server role only: creates `~/Server` and refreshes the links of whatever services this machine has enabled |
|
||||
|
||||
A server runs the shared stages plus its own two, and skips the rest —
|
||||
@@ -235,13 +235,10 @@ server/ The server role: compose services (one directory per
|
||||
service), the nightly image updater, and its units. See
|
||||
server/README.md
|
||||
skills/ Agent skills for operating this desktop, linked into
|
||||
~/.agents/skills and ~/.claude/skills
|
||||
~/.claude/skills
|
||||
setup/
|
||||
apps/ Applications built from source, one file each
|
||||
keys/ Pinned signing keys, for publishers that ship no fetchable
|
||||
one; setup/keys/README.md records where each came from
|
||||
lib/ Shared by more than one stage; the extras catalog reader,
|
||||
the machine role, the verified ChatGPT repository
|
||||
lib/ Shared by more than one stage; the extras catalog reader
|
||||
packages/ One package per line; extras/ holds the optional categories
|
||||
scripts/ Run in order by ./install
|
||||
tests/ Contracts. See below
|
||||
@@ -250,7 +247,7 @@ docs/ Settings reference, and the design specs behind the work
|
||||
|
||||
## Tests
|
||||
|
||||
192 of them, under `tests/`. `tests/contracts.manifest` classifies every
|
||||
188 of them, under `tests/`. `tests/contracts.manifest` classifies every
|
||||
contract by the capabilities it needs. Run the hermetic set, or grant a
|
||||
specific external capability when automation needs it:
|
||||
|
||||
|
||||
+2
-8
@@ -134,6 +134,7 @@ ${BOLD}Examples:${RESET}
|
||||
$PROGRAM upgrade
|
||||
$PROGRAM apps
|
||||
$PROGRAM app
|
||||
$PROGRAM app chatgpt-desktop
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -450,16 +451,11 @@ CONTRACT_CAPABILITIES=(hermetic live-host live-compositor live-desktop network p
|
||||
|
||||
contract_paths() {
|
||||
local candidate
|
||||
# The manifest is kept in byte order, so both the discovery sort and the
|
||||
# comparison below have to be byte order too. A UTF-8 collation folds the
|
||||
# punctuation away -- `calendar_agenda_bridge_test.py` sorts before
|
||||
# `calendar-agenda-helper-contract` under en_US and after it under C -- and
|
||||
# a gate that passes or fails on the machine's LANG is not a gate.
|
||||
while IFS= read -r candidate; do
|
||||
[[ -x "$candidate" || "$candidate" == *_test.py ]] || continue
|
||||
printf 'tests/%s\n' "${candidate#"$PANAMA_DIR/tests/"}"
|
||||
done < <(find "$PANAMA_DIR/tests" -type f \
|
||||
-not -path '*/fixtures/*' -not -path '*__pycache__*' | LC_ALL=C sort)
|
||||
-not -path '*/fixtures/*' -not -path '*__pycache__*' | sort)
|
||||
}
|
||||
|
||||
contract_manifest_entries() {
|
||||
@@ -482,8 +478,6 @@ validate_contract_manifest() {
|
||||
require_contract_manifest || return 1
|
||||
|
||||
local manifest="$PANAMA_DIR/$CONTRACT_MANIFEST"
|
||||
# Byte order, for the same reason contract_paths sorts in it.
|
||||
local LC_ALL=C
|
||||
local line capabilities path extra previous_comment="" previous_was_comment=0
|
||||
local previous_path="" capability discovered
|
||||
local -a capability_list=() findings=()
|
||||
|
||||
+3
-3
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# panama-server – the compose services a server machine runs.
|
||||
#
|
||||
# The repository carries the catalog: server/vps/<Name>/ holds a
|
||||
# The repository carries the catalog: server/containers/<Name>/ holds a
|
||||
# compose.yml, a podman-<name>.service user unit, and an .env.example naming
|
||||
# what the service needs told. The machine carries the rest: ~/Server/<Name>/
|
||||
# is a real directory owning the .env (secrets never sit inside the checkout,
|
||||
@@ -27,7 +27,7 @@ PANAMA_DIR=$(cd "$(dirname "$SCRIPT_PATH")/.." && pwd)
|
||||
|
||||
# Overridable so the contract can point this at a fixture catalog and a
|
||||
# throwaway HOME rather than the machine's real services.
|
||||
SERVER_DIR="${PANAMA_SERVER_DIR:-$PANAMA_DIR/server/vps}"
|
||||
SERVER_DIR="${PANAMA_SERVER_DIR:-$PANAMA_DIR/server/containers}"
|
||||
TARGET_DIR="${PANAMA_SERVER_TARGET:-$HOME/Server}"
|
||||
UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
||||
STATE_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/panama/server-definitions"
|
||||
@@ -46,7 +46,7 @@ err() { printf '%s✗%s %s\n' "${RED}${BOLD}" "$RESET" "$*" >&2; }
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
${BOLD}$PROGRAM${RESET} – manage the compose services in server/vps/
|
||||
${BOLD}$PROGRAM${RESET} – manage the compose services in server/containers/
|
||||
|
||||
${BOLD}Usage:${RESET}
|
||||
$PROGRAM list Every service in the catalog, and its state here
|
||||
|
||||
@@ -45,6 +45,65 @@ checkout_command() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Git's index hints are performance promises, not trust evidence. In
|
||||
# particular, assume-unchanged and skip-worktree can make porcelain status
|
||||
# report a clean checkout whose files no longer match HEAD. Compare every
|
||||
# tracked blob and Git mode with the verified commit before handing control to
|
||||
# any file in the worktree.
|
||||
checkout_matches_verified_commit() (
|
||||
local checkout="$1" listing="" entry metadata mode type expected path actual
|
||||
local link_target_with_sentinel link_target
|
||||
|
||||
trap '[[ -z "$listing" ]] || rm -f -- "$listing"' EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
listing="$(mktemp -u -t panama-boot-tree.XXXXXX)" || exit 1
|
||||
umask 077
|
||||
if ! (set -o noclobber; : >"$listing") 2>/dev/null; then
|
||||
listing=""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
checkout_command git -C "$checkout" ls-tree -rz --full-tree \
|
||||
"$PANAMA_BOOT_REVISION" >"$listing" || exit 1
|
||||
while IFS= read -r -d '' entry; do
|
||||
[[ "$entry" == *$'\t'* ]] || exit 1
|
||||
metadata="${entry%%$'\t'*}"
|
||||
path="${entry#*$'\t'}"
|
||||
read -r mode type expected <<<"$metadata"
|
||||
[[ "$type" == blob && -n "$path" && "$path" != /* ]] || exit 1
|
||||
|
||||
case "$mode" in
|
||||
100644) [[ -f "$checkout/$path" && ! -L "$checkout/$path" \
|
||||
&& ! -x "$checkout/$path" ]] || exit 1 ;;
|
||||
100755) [[ -f "$checkout/$path" && ! -L "$checkout/$path" \
|
||||
&& -x "$checkout/$path" ]] || exit 1 ;;
|
||||
120000)
|
||||
[[ -L "$checkout/$path" ]] || exit 1
|
||||
# hash-object given a pathname follows a symlink. Git's 120000 blob is
|
||||
# the link text itself, including any trailing newlines, so preserve
|
||||
# those bytes with a sentinel and hash stdin instead.
|
||||
link_target_with_sentinel="$(
|
||||
readlink -n -- "$checkout/$path" && printf .
|
||||
)" || exit 1
|
||||
[[ "$link_target_with_sentinel" == *. ]] || exit 1
|
||||
link_target="${link_target_with_sentinel%.}"
|
||||
actual="$(
|
||||
printf '%s' "$link_target" \
|
||||
| checkout_command git -C "$checkout" hash-object --stdin
|
||||
)" || exit 1
|
||||
[[ "$actual" == "$expected" ]] || exit 1
|
||||
continue
|
||||
;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
|
||||
actual="$(checkout_command git -C "$checkout" hash-object --no-filters -- "$path")" \
|
||||
|| exit 1
|
||||
[[ "$actual" == "$expected" ]] || exit 1
|
||||
done <"$listing"
|
||||
)
|
||||
|
||||
prepare_panama_checkout() {
|
||||
local checkout="$1" actual_head checkout_status
|
||||
|
||||
@@ -110,6 +169,30 @@ for arg in "$@"; do
|
||||
esac
|
||||
done
|
||||
|
||||
# Keep the worktree comparison at the last possible boundary. Checkout
|
||||
# preparation may invoke several commands and return to the caller; performing
|
||||
# the byte/mode/link check here ensures a change in that interval is rejected
|
||||
# before any tracked file is executed.
|
||||
verified_install_handoff() {
|
||||
local use_tty="$1"
|
||||
if ! checkout_matches_verified_commit "$PANAMA_PATH"; then
|
||||
echo "boot: checkout files do not match PANAMA_BOOT_REVISION" >&2
|
||||
return 1
|
||||
fi
|
||||
if [[ -n "$BOOTSTRAP_USER" ]]; then
|
||||
if (( use_tty )); then
|
||||
exec runuser -u "$BOOTSTRAP_USER" -- env PANAMA_PATH="$PANAMA_PATH" \
|
||||
"$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"} </dev/tty
|
||||
fi
|
||||
exec runuser -u "$BOOTSTRAP_USER" -- env PANAMA_PATH="$PANAMA_PATH" \
|
||||
"$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"}
|
||||
fi
|
||||
if (( use_tty )); then
|
||||
exec "$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"} </dev/tty
|
||||
fi
|
||||
exec "$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"}
|
||||
}
|
||||
|
||||
# The public bootstrap contract runs this branch as an ordinary user with a
|
||||
# stubbed root identity. Keep its filesystem adapter unavailable to a real root
|
||||
# shell so it cannot redirect a real installation by accident.
|
||||
@@ -471,7 +554,7 @@ if [[ "$(id -u)" -eq 0 ]]; then
|
||||
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
echo "Installing git, which the clone needs"
|
||||
dnf install -y git
|
||||
dnf install -y --repo=fedora --repo=updates --from-repo=fedora,updates git
|
||||
fi
|
||||
|
||||
# Create or advance the checkout as the target user. A root-owned .git in a
|
||||
@@ -481,15 +564,14 @@ if [[ "$(id -u)" -eq 0 ]]; then
|
||||
prepare_panama_checkout "$PANAMA_PATH"
|
||||
|
||||
echo "Handing off to install as $username"
|
||||
exec runuser -u "$username" -- env PANAMA_PATH="$PANAMA_PATH" \
|
||||
"$PANAMA_PATH/install" --server </dev/tty
|
||||
verified_install_handoff 1
|
||||
fi
|
||||
|
||||
# git is the one dependency the clone itself needs. Everything else -- gum
|
||||
# included -- is bootstrapped by `install`.
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
echo "Installing git, which the clone needs"
|
||||
sudo dnf install -y git
|
||||
sudo dnf install -y --repo=fedora --repo=updates --from-repo=fedora,updates git
|
||||
fi
|
||||
|
||||
prepare_panama_checkout "$PANAMA_PATH"
|
||||
@@ -500,7 +582,8 @@ prepare_panama_checkout "$PANAMA_PATH"
|
||||
# so itself.
|
||||
# The probe actually opens /dev/tty rather than testing -r: a process with no
|
||||
# controlling terminal passes -r and then fails the redirect.
|
||||
handoff_tty=0
|
||||
if [[ ! -t 0 ]] && (exec </dev/tty) 2>/dev/null; then
|
||||
exec "$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"} </dev/tty
|
||||
handoff_tty=1
|
||||
fi
|
||||
exec "$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"}
|
||||
verified_install_handoff "$handoff_tty"
|
||||
+2
-3
@@ -22,7 +22,6 @@ alias avante='nvim -c "lua vim.defer_fn(function()require(\"avante.api\").zen_mo
|
||||
alias nlc='nvim leetcode.nvim'
|
||||
alias clauded='claude --allow-dangerously-skip-permissions'
|
||||
alias claudedr='claude --allow-dangerously-skip-permissions --resume'
|
||||
alias klaude='CLAUDE_CONFIG_DIR="$HOME/.klaude" claude'
|
||||
alias commit-repo='git add -A && git commit -m "Update stuff" && git push'
|
||||
|
||||
# Docker Shortcuts
|
||||
@@ -37,9 +36,9 @@ alias fprint-on='sudo authselect enable-feature with-fingerprint'
|
||||
alias fprint-off='sudo authselect disable-feature with-fingerprint'
|
||||
|
||||
# File system
|
||||
alias ls='eza -lh --group-directories-first --icons=auto'
|
||||
alias ls='eza -lh --group-directories-first --icons'
|
||||
alias lsa='ls -a'
|
||||
alias lt='eza --tree --level=2 --long --icons=auto --git'
|
||||
alias lt='eza --tree --level=2 --long --icons --git'
|
||||
alias lta='lt -a'
|
||||
# Fedora's bat installs /usr/bin/bat; batcat is the Debian name.
|
||||
alias ff="fzf --preview 'bat --style=numbers --color=always {}'"
|
||||
|
||||
+2
-17
@@ -16,23 +16,8 @@ unset PANAMA_ENV
|
||||
# Editor used by CLI
|
||||
export EDITOR="nvim"
|
||||
export SUDO_EDITOR="$EDITOR"
|
||||
# A graphical askpass, and only where there is a session to draw it in.
|
||||
# SSH_ASKPASS_REQUIRE=prefer means ssh and git reach for the dialog BEFORE the
|
||||
# terminal prompt, so on a headless machine the prompt that would have worked
|
||||
# is skipped in favour of one that cannot open: `git push` then fails reading
|
||||
# a username rather than asking for it. The desktop keeps the dialog; a
|
||||
# server, a serial console and a plain SSH session fall back to the terminal.
|
||||
if [ -n "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ] && [ -x /usr/bin/ksshaskpass ]; then
|
||||
export SSH_ASKPASS=/usr/bin/ksshaskpass
|
||||
export SSH_ASKPASS_REQUIRE=prefer
|
||||
else
|
||||
# Cleared, not merely left unset. An askpass inherited from a parent that
|
||||
# did have a display -- an agent's shell, a forwarded environment, a login
|
||||
# that predates this guard -- is exactly as unusable here as one this file
|
||||
# set itself, and declining to add it does nothing about a value already in
|
||||
# the environment.
|
||||
unset SSH_ASKPASS SSH_ASKPASS_REQUIRE
|
||||
fi
|
||||
export SSH_ASKPASS=/usr/bin/ksshaskpass
|
||||
export SSH_ASKPASS_REQUIRE=prefer
|
||||
|
||||
# Define Paths
|
||||
export CARGO_PATH="$HOME/.cargo"
|
||||
|
||||
@@ -244,20 +244,10 @@ local shipped_mode = "4500x3000@60"
|
||||
local shipped_scale = 1.5
|
||||
local shipped_transform = 0
|
||||
|
||||
-- 8-bit output. 4500x3000@60 at 10bpc is ~24 Gbps, right at the edge of DP 1.4
|
||||
-- HBR3 and reliant on DSC, and this panel's link is marginal: every modeset
|
||||
-- retrains it and blanks the screen. 8bpc keeps headroom on the link.
|
||||
--
|
||||
-- Related: directScanoutPolicy is 0 in Panama settings (2026-09-13). With
|
||||
-- scanout on, a fullscreen game whose buffer depth differs from the desktop
|
||||
-- (games ship both 8- and 10-bit swapchains) makes Hyprland change the output
|
||||
-- format, and on amdgpu a format change is a full modeset. Compositing always
|
||||
-- keeps the format fixed, so the link never retrains mid-game.
|
||||
--
|
||||
-- vrrPolicy is also 0 there. VRR on this panel loses sync and blacks out
|
||||
-- (seen on GNOME in July 2026 and again here); a 60Hz panel gains little
|
||||
-- from it anyway.
|
||||
local shipped_bitdepth = 8
|
||||
-- 10-bit output. 4500x3000@60 at 10bpc is ~24 Gbps, right at the edge of DP 1.4
|
||||
-- HBR3, so this relies on DSC. If the display fails to light up or falls back to
|
||||
-- a lower mode, drop this to 8 first.
|
||||
local shipped_bitdepth = 10
|
||||
|
||||
-- "auto" = sRGB at 8bpc, wide gamut at 10bpc. Not HDR; see header.
|
||||
local shipped_cm = "auto"
|
||||
|
||||
@@ -125,16 +125,6 @@ hl.window_rule({
|
||||
no_dim = true,
|
||||
})
|
||||
|
||||
-- Steam games never get true fullscreen. Everything Hyprland does only for a
|
||||
-- real fullscreen window (direct scanout, VRR, tearing, auto HDR) makes this
|
||||
-- panel's marginal DP link retrain and blank (2026-09-13). "1 2" keeps the
|
||||
-- window maximized internally while the game believes it is fullscreen, which
|
||||
-- is what borderless windowed looks like from the game's side.
|
||||
hl.window_rule({
|
||||
match = { class = "^steam_app_\\d+$" },
|
||||
fullscreen_state = "1 2",
|
||||
})
|
||||
|
||||
-- Steam itself is a normal window, but its transient popups are a mess.
|
||||
hl.window_rule({
|
||||
match = { class = "^steam$", title = "^(Friends List|Steam Settings|Special Offer.*)$" },
|
||||
@@ -279,28 +269,16 @@ hl.layer_rule({
|
||||
ignore_alpha = 0.2,
|
||||
})
|
||||
|
||||
-- Overview and local screen-reading UI dim the desktop behind them.
|
||||
-- Overview, capture and local screen-reading UI dim the desktop behind them.
|
||||
hl.layer_rule({
|
||||
name = "qs-overlay",
|
||||
match = { namespace = "^qs-(overview|screen-intelligence)$" },
|
||||
match = { namespace = "^qs-(overview|capture|screen-intelligence)$" },
|
||||
blur = true,
|
||||
ignore_alpha = 0.4,
|
||||
dim_around = true,
|
||||
no_screen_share = true,
|
||||
})
|
||||
|
||||
-- The screenshot picker paints an opaque frozen frame edge to edge, so blur and
|
||||
-- dim would be compositor work on pixels nobody sees. no_anim is load-bearing:
|
||||
-- services/Capture.qml unmaps the picker and runs grim 90ms later, and the
|
||||
-- 200ms layersOut fade would otherwise put the half-faded picker, dimming and
|
||||
-- all, into every screenshot.
|
||||
hl.layer_rule({
|
||||
name = "qs-capture",
|
||||
match = { namespace = "^qs-capture$" },
|
||||
no_anim = true,
|
||||
no_screen_share = true,
|
||||
})
|
||||
|
||||
-- Notification toasts. Blurred like every other shell surface -- without this
|
||||
-- the cards are a near-transparent fill sitting directly on the wallpaper and
|
||||
-- read as washed out rather than as glass.
|
||||
|
||||
@@ -1805,7 +1805,7 @@ Singleton {
|
||||
"org.mozilla.thunderbird_esr", "com.slack.Slack",
|
||||
"app.bluebubbles.BlueBubbles", "rustdesk",
|
||||
"io.podman_desktop.PodmanDesktop", "com.anthropic.Claude",
|
||||
"chatgpt", "md.obsidian.Obsidian",
|
||||
"codex-desktop", "md.obsidian.Obsidian",
|
||||
"com.obsproject.Studio", "steam"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -27,13 +27,9 @@ PanelWindow {
|
||||
right: true
|
||||
}
|
||||
|
||||
// Ignore, and nothing else: reserve no space (or every window on the
|
||||
// workspace resizes as the picker opens and closes) and respect nobody's
|
||||
// (or the window starts under the bar, the full-output freeze frame is
|
||||
// squeezed into a shorter box, and every selection lands one bar-height
|
||||
// off in the real capture). Never pair this with exclusiveZone; see
|
||||
// tests/quickshell/exclusion-idiom-contract.
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
// Fullscreen overlays must not reserve space, or every window on the
|
||||
// workspace gets resized as the picker opens and closes.
|
||||
exclusiveZone: 0
|
||||
|
||||
WlrLayershell.namespace: "qs-capture" // matched by a layerrule in hypr/rules.lua
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
|
||||
@@ -57,6 +57,7 @@ PanelWindow {
|
||||
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
color: "transparent"
|
||||
exclusiveZone: 0
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
// The `^qs-popover` prefix rule in hypr/rules.lua blurs what is behind
|
||||
|
||||
@@ -55,6 +55,7 @@ PanelWindow {
|
||||
color: "transparent"
|
||||
|
||||
// A dock that reserved space would not be intellihiding.
|
||||
exclusiveZone: 0
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
// Matched by the `qs-dock` layer rule in hypr/rules.lua — do not rename.
|
||||
|
||||
@@ -40,6 +40,7 @@ PanelWindow {
|
||||
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
color: "transparent"
|
||||
exclusiveZone: 0
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
// Blurred by the `^qs-popover` rule in hypr/rules.lua; the scrim is painted
|
||||
|
||||
@@ -44,6 +44,7 @@ PanelWindow {
|
||||
// Reserve nothing and respect nothing: the glow is drawn over the whole
|
||||
// output including under the bar and the dock, which is what makes it
|
||||
// visible from wherever the eyes happen to be.
|
||||
exclusiveZone: 0
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
color: "transparent"
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ PanelWindow {
|
||||
screen: root.modelData
|
||||
anchors.bottom: true
|
||||
margins.bottom: Theme.dockIconSize + Theme.dockPadding * 2 + Theme.barGap * 3
|
||||
exclusiveZone: 0
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
implicitWidth: root.desiredWidth
|
||||
implicitHeight: 64
|
||||
|
||||
@@ -22,6 +22,7 @@ PanelWindow {
|
||||
}
|
||||
|
||||
color: "transparent"
|
||||
exclusiveZone: 0
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
// Matched by the `qs-overlay` layer rule in hypr/rules.lua — do not rename.
|
||||
|
||||
@@ -34,23 +34,11 @@ Item {
|
||||
visible: root.rowCount > 0
|
||||
implicitHeight: rows.implicitHeight
|
||||
|
||||
// Probing I2C takes on the order of a second, so it waits until the quick
|
||||
// settings panel is actually on screen rather than running at shell
|
||||
// startup. Monitors do not come and go, so once is enough.
|
||||
//
|
||||
// The trigger is the panel opening, not this item becoming visible: on a
|
||||
// machine with no backlight, `visible` stays false until a scan has found
|
||||
// a DDC display, so keying the scan off it would never scan at all.
|
||||
Connections {
|
||||
target: ShellState
|
||||
function onQuickSettingsOpenChanged(): void { root.scanIfOpen(); }
|
||||
}
|
||||
Component.onCompleted: root.scanIfOpen()
|
||||
|
||||
function scanIfOpen(): void {
|
||||
if (ShellState.quickSettingsOpen && !Brightness.scanned)
|
||||
Brightness.refresh();
|
||||
}
|
||||
// Probing I2C takes on the order of a second, so it waits until the panel
|
||||
// is actually on screen rather than running at shell startup. Monitors do
|
||||
// not come and go, so once is enough.
|
||||
onVisibleChanged: if (visible && !Brightness.scanned) Brightness.refresh()
|
||||
Component.onCompleted: if (root.visible && !Brightness.scanned) Brightness.refresh()
|
||||
|
||||
// `-m` is the machine-readable form: name,class,current,percent,max
|
||||
Process {
|
||||
|
||||
@@ -22,6 +22,7 @@ Variants {
|
||||
implicitWidth: 260
|
||||
implicitHeight: 172
|
||||
color: "transparent"
|
||||
exclusiveZone: 0
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
mask: Region {}
|
||||
|
||||
|
||||
@@ -383,13 +383,6 @@ Lua-configured Hyprland, prints the refusal to stdout, and exits 0. `eval` exits
|
||||
0 on syntax and runtime errors too. The only trustworthy signal that a write
|
||||
landed is reading the value back.
|
||||
|
||||
**`exclusionMode: ExclusionMode.Ignore` goes alone.** Quickshell's `exclusiveZone`
|
||||
setter flips `exclusionMode` back to `Normal` as a side effect, so a window that
|
||||
declares both is at the mercy of which property the QML engine applies last.
|
||||
The 2026-09-14 Qt update changed that order and every full-screen overlay slid
|
||||
under the bar. `tests/quickshell/exclusion-idiom-contract` fails any file that
|
||||
pairs them.
|
||||
|
||||
**The Settings window is tiled.** `implicitWidth` is a hint; the layout decides,
|
||||
and it ranges from a half-screen split to the full display. `SliderRow` stacks
|
||||
its control under the label below 520px. Test narrow.
|
||||
|
||||
@@ -78,6 +78,7 @@ PanelWindow {
|
||||
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
color: "transparent"
|
||||
exclusiveZone: 0
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
WlrLayershell.namespace: "qs-popover-welcome"
|
||||
|
||||
@@ -341,10 +341,7 @@ esac'
|
||||
property bool record: false
|
||||
property bool intelligence: false
|
||||
// A handful of frames at 60Hz, enough for the compositor to recomposite
|
||||
// the output without the overlay on it. Only enough because the
|
||||
// qs-capture layer rule in hypr/rules.lua sets no_anim: with the
|
||||
// default 200ms layersOut fade the half-faded picker would still be
|
||||
// on screen when grim reads it.
|
||||
// the output without the overlay on it.
|
||||
interval: 90
|
||||
onTriggered: {
|
||||
const r = commitDelay.rect;
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/usr/bin/env zsh
|
||||
# The Mac mini's shell. macOS stays on zsh: its bash is 3.2, and
|
||||
# config/bash/shell leans on Linux-only tools (stat -c, ksshaskpass, the nvm
|
||||
# profile script), so this is a small zsh counterpart rather than a port.
|
||||
# Linked to ~/.zshrc. Homebrew's PATH comes from ~/.zprofile (brew shellenv).
|
||||
export PANAMA_PATH="$HOME/.local/share/Panama"
|
||||
|
||||
export EDITOR="nvim"
|
||||
export SUDO_EDITOR="$EDITOR"
|
||||
|
||||
export BUN_INSTALL="$HOME/.bun"
|
||||
export PATH="$HOME/.local/bin:$BUN_INSTALL/bin:$PANAMA_PATH/bin:$PATH"
|
||||
|
||||
# History shared across sessions, so tmux panes see each other's commands.
|
||||
HISTFILE="$HOME/.zsh_history"
|
||||
HISTSIZE=50000
|
||||
SAVEHIST=50000
|
||||
setopt SHARE_HISTORY HIST_IGNORE_DUPS HIST_IGNORE_SPACE
|
||||
|
||||
# Emacs keys, like bash's readline: ctrl-a/e/r work as they do on Linux.
|
||||
bindkey -e
|
||||
|
||||
autoload -Uz compinit && compinit
|
||||
[ -s "$BUN_INSTALL/_bun" ] && source "$BUN_INSTALL/_bun"
|
||||
|
||||
# Same SSH behaviour as config/bash/shell: an SSH login lands in a tmux
|
||||
# session that survives a dropped connection. PANAMA_SSH_TMUX=off opts out.
|
||||
if [[ -n "$SSH_CONNECTION" && -z "$TMUX" && -o interactive &&
|
||||
"${PANAMA_SSH_TMUX:-on}" != "off" ]] && command -v tmux >/dev/null 2>&1; then
|
||||
exec tmux new-session -A -s main
|
||||
fi
|
||||
|
||||
alias c="clear"
|
||||
alias n="nvim"
|
||||
alias n.="nvim ."
|
||||
alias ..="cd .."
|
||||
alias ...="cd ../.."
|
||||
alias ....="cd ../../.."
|
||||
alias letscode="ranger ~/Documents/Documents/Code"
|
||||
alias commit-repo='git add -A && git commit -m "Update stuff" && git push'
|
||||
alias ls='eza -lh --group-directories-first --icons=auto'
|
||||
alias lsa='ls -a'
|
||||
alias lt='eza --tree --level=2 --long --icons=auto --git'
|
||||
alias ff="fzf --preview 'bat --style=numbers --color=always {}'"
|
||||
compress() { tar -czf "${1%/}.tar.gz" "${1%/}"; }
|
||||
alias decompress="tar -xzf"
|
||||
|
||||
# Xcode cannot sign from an SSH session while the login keychain is locked
|
||||
# (errSecInternalComponent). Run this once per session before a build.
|
||||
alias unlock-keychain='security unlock-keychain ~/Library/Keychains/login.keychain-db'
|
||||
|
||||
eval "$(zoxide init zsh)"
|
||||
source <(fzf --zsh)
|
||||
eval "$(oh-my-posh init zsh --config "$PANAMA_PATH/config/dot/ohmyposh/gib.omp.json")"
|
||||
@@ -71,7 +71,7 @@ source "$PANAMA_PATH/bin/ascii"
|
||||
# time. On an upgrade it is worth running only when its package lists or
|
||||
# reviewed installer trust inputs changed, so this hashes them and remembers
|
||||
# the result. The framed, sorted stream includes top-level package files, the
|
||||
# package-stage adapter, every helper it sources, and regular provenance files;
|
||||
# package-stage adapter, the provenance helper, and regular provenance files;
|
||||
# both relative paths and bytes are part of the state.
|
||||
#
|
||||
# A content hash rather than a git range, because Panama is developed in place:
|
||||
@@ -88,42 +88,75 @@ source "$PANAMA_PATH/bin/ascii"
|
||||
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/panama"
|
||||
PACKAGES_HASH="$STATE_DIR/packages-hash"
|
||||
|
||||
hash_packages() {
|
||||
local file relative size fixed_input digest
|
||||
|
||||
for fixed_input in \
|
||||
"$PANAMA_PATH/setup/scripts/install-packages" \
|
||||
"$PANAMA_PATH/setup/lib/artifact-provenance" \
|
||||
"$PANAMA_PATH/setup/lib/chatgpt-package" \
|
||||
"$PANAMA_PATH/setup/lib/extras-catalog" \
|
||||
"$PANAMA_PATH/setup/lib/machine-role"; do
|
||||
[[ -f "$fixed_input" && ! -L "$fixed_input" && -r "$fixed_input" ]] || return 1
|
||||
done
|
||||
|
||||
digest="$(
|
||||
{
|
||||
find "$PANAMA_PATH/setup/packages" -maxdepth 1 -type f -print0 || exit 1
|
||||
printf '%s\0' \
|
||||
"$PANAMA_PATH/setup/scripts/install-packages" \
|
||||
"$PANAMA_PATH/setup/lib/artifact-provenance" \
|
||||
"$PANAMA_PATH/setup/lib/chatgpt-package" \
|
||||
"$PANAMA_PATH/setup/lib/extras-catalog" \
|
||||
"$PANAMA_PATH/setup/lib/machine-role" || exit 1
|
||||
find "$PANAMA_PATH/setup/provenance" -type f -print0 || exit 1
|
||||
} | LC_ALL=C sort -z | while IFS= read -r -d '' file; do
|
||||
relative="${file#"$PANAMA_PATH"/}"
|
||||
size="$(wc -c <"$file")" || exit 1
|
||||
printf '%s\0%s\0' "$relative" "$size" || exit 1
|
||||
cat -- "$file" || exit 1
|
||||
printf '\0' || exit 1
|
||||
done | sha256sum | cut -d' ' -f1
|
||||
)" || return 1
|
||||
printf '%s\n' "$digest"
|
||||
_collect_package_inputs() {
|
||||
local destination="$1" raw="${1}.raw"
|
||||
[[ -d "$PANAMA_PATH/setup/packages" \
|
||||
&& ! -L "$PANAMA_PATH/setup/packages" \
|
||||
&& -d "$PANAMA_PATH/setup/provenance" \
|
||||
&& ! -L "$PANAMA_PATH/setup/provenance" ]] || return 1
|
||||
{
|
||||
printf '%s\0' \
|
||||
"$PANAMA_PATH/setup/scripts/install-packages" \
|
||||
"$PANAMA_PATH/setup/lib/artifact-provenance" \
|
||||
"$PANAMA_PATH/setup/lib/extras-catalog" \
|
||||
"$PANAMA_PATH/setup/lib/machine-role" || exit 1
|
||||
# extras/ is deliberately excluded. A symlink or other non-directory
|
||||
# object at this level is still an input error, not something discovery may
|
||||
# silently omit.
|
||||
find "$PANAMA_PATH/setup/packages" -mindepth 1 -maxdepth 1 \
|
||||
! -type d -print0 || exit 1
|
||||
find "$PANAMA_PATH/setup/provenance" -mindepth 1 \
|
||||
! -type d -print0 || exit 1
|
||||
} >"$raw" || return 1
|
||||
LC_ALL=C sort -z "$raw" >"$destination"
|
||||
}
|
||||
|
||||
packages_needed() {
|
||||
local current_hash="$1" recorded_hash
|
||||
_write_package_manifest() {
|
||||
local inputs="$1" destination="$2" file relative digest
|
||||
: >"$destination" || return 1
|
||||
while IFS= read -r -d '' file; do
|
||||
[[ -f "$file" && ! -L "$file" && -r "$file" ]] || return 1
|
||||
relative="${file#"$PANAMA_PATH"/}"
|
||||
[[ "$relative" != "$file" ]] || return 1
|
||||
digest="$(sha256sum -- "$file" | awk '{ print $1 }')" || return 1
|
||||
[[ "$digest" =~ ^[0-9a-f]{64}$ ]] || return 1
|
||||
printf '%s\0%s\0' "$relative" "$digest" >>"$destination" || return 1
|
||||
done <"$inputs"
|
||||
}
|
||||
|
||||
# Read every input twice from the same enumerated set. A file or path that
|
||||
# changes while the snapshot is built cannot produce a receipt.
|
||||
hash_packages() (
|
||||
local work="" inputs_before inputs_after manifest_before manifest_after
|
||||
trap '[[ -z "$work" ]] || rm -rf -- "$work"' EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
work="$(mktemp -u -d -t panama-packages-hash.XXXXXX)" || exit 1
|
||||
if ! mkdir -m 700 -- "$work"; then
|
||||
work=""
|
||||
exit 1
|
||||
fi
|
||||
inputs_before="$work/inputs-before"
|
||||
inputs_after="$work/inputs-after"
|
||||
manifest_before="$work/manifest-before"
|
||||
manifest_after="$work/manifest-after"
|
||||
|
||||
_collect_package_inputs "$inputs_before" || exit 1
|
||||
_write_package_manifest "$inputs_before" "$manifest_before" || exit 1
|
||||
_collect_package_inputs "$inputs_after" || exit 1
|
||||
cmp -s -- "$inputs_before" "$inputs_after" || exit 1
|
||||
_write_package_manifest "$inputs_after" "$manifest_after" || exit 1
|
||||
cmp -s -- "$manifest_before" "$manifest_after" || exit 1
|
||||
sha256sum -- "$manifest_before" | awk '{ print $1 }'
|
||||
)
|
||||
|
||||
PACKAGE_START_HASH=""
|
||||
|
||||
packages_needed() {
|
||||
local current_hash recorded_hash
|
||||
|
||||
current_hash="$(hash_packages)" || return 2
|
||||
PACKAGE_START_HASH="$current_hash"
|
||||
(( FORCE_PACKAGES )) && return 0
|
||||
(( UPGRADE )) || return 0
|
||||
[[ -r "$PACKAGES_HASH" ]] || return 0
|
||||
@@ -134,52 +167,41 @@ packages_needed() {
|
||||
# Written only after the stage succeeds, mirroring the rule panama-migrate
|
||||
# documents for its markers: a step that did not complete has not happened, and
|
||||
# recording it as done hides it forever.
|
||||
record_packages_hash() {
|
||||
local starting_hash="$1" current_hash temporary_hash
|
||||
record_packages_hash() (
|
||||
local expected_hash="$1" current_hash temporary_hash=""
|
||||
trap '[[ -z "$temporary_hash" ]] || rm -f -- "$temporary_hash"' EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
[[ "$expected_hash" =~ ^[0-9a-f]{64}$ ]] || return 1
|
||||
current_hash="$(hash_packages)" || return 1
|
||||
[[ "$current_hash" == "$starting_hash" ]] || return 1
|
||||
[[ "$current_hash" == "$expected_hash" ]] || return 1
|
||||
mkdir -p "$STATE_DIR"
|
||||
temporary_hash="$(mktemp "$STATE_DIR/.packages-hash.XXXXXX")" || return 1
|
||||
if printf '%s\n' "$starting_hash" >"$temporary_hash"; then
|
||||
mv -f -- "$temporary_hash" "$PACKAGES_HASH"
|
||||
else
|
||||
rm -f -- "$temporary_hash"
|
||||
temporary_hash="$(mktemp -u "$STATE_DIR/.packages-hash.XXXXXX")" || return 1
|
||||
umask 077
|
||||
if ! (set -o noclobber; : >"$temporary_hash") 2>/dev/null; then
|
||||
temporary_hash=""
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
printf '%s\n' "$expected_hash" >"$temporary_hash" || return 1
|
||||
mv -f -- "$temporary_hash" "$PACKAGES_HASH" || return 1
|
||||
temporary_hash=""
|
||||
)
|
||||
|
||||
# Repository trust is checked before the installer can reach its bootstrap DNF.
|
||||
# Status 78 is reserved for a trust-root failure. It suppresses every stage that
|
||||
# opens DNF -- install-hardware included, which would otherwise pull drivers
|
||||
# through the very repository in doubt -- and the run still exits 78 at the end.
|
||||
#
|
||||
# It suppresses nothing else. Linking dotfiles, skills and user content reads no
|
||||
# repository and installs no package, and a machine whose Terra is in question
|
||||
# still wants its configuration. Refusing the safe work because the unsafe work
|
||||
# is unavailable does not make the machine safer, it just leaves the machine
|
||||
# unconfigured with no way to fix itself. Exiting here instead meant link-skills
|
||||
# never ran on a machine whose Terra was merely unadopted, so ~/.claude/skills
|
||||
# stayed the whole-directory symlink it had been before skills were linked one
|
||||
# by one, and not one shipped skill was reachable.
|
||||
# Status 78 is reserved for a trust-root failure and is propagated unchanged so
|
||||
# no later stage, especially install-hardware, can invoke DNF with that repo.
|
||||
TERRA_TRUST_FAILURE_STATUS=78
|
||||
DNF_STAGES=(install-packages change-settings install-hardware)
|
||||
package_trust_refused=0
|
||||
|
||||
stage_opens_dnf() {
|
||||
local candidate="$1" dnf_stage
|
||||
for dnf_stage in "${DNF_STAGES[@]}"; do
|
||||
[[ "$candidate" == "$dnf_stage" ]] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
trust_preflight="$PANAMA_PATH/setup/scripts/install-packages"
|
||||
if [[ ! -x "$trust_preflight" ]]; then
|
||||
printf 'install: package repository trust preflight is unavailable\n' >&2
|
||||
package_trust_refused=1
|
||||
elif ! "$trust_preflight" --trust-preflight; then
|
||||
exit "$TERRA_TRUST_FAILURE_STATUS"
|
||||
fi
|
||||
if "$trust_preflight" --trust-preflight; then
|
||||
:
|
||||
else
|
||||
trust_status=$?
|
||||
printf 'install: package repository trust preflight failed\n' >&2
|
||||
package_trust_refused=1
|
||||
exit "$trust_status"
|
||||
fi
|
||||
|
||||
# ── The interview ────────────────────────────────────────────────────────────
|
||||
@@ -199,7 +221,7 @@ fi
|
||||
# Gated exactly like the interview itself: under --upgrade no questions are
|
||||
# asked, so nothing here is used, and a machine that cannot install gum must
|
||||
# not have that stop an upgrade that never needed it.
|
||||
if (( ! UPGRADE && ! package_trust_refused )); then
|
||||
if (( ! UPGRADE )); then
|
||||
bootstrap=()
|
||||
command -v gum >/dev/null 2>&1 || bootstrap+=(gum)
|
||||
# The probe tools serve only the hardware questions, which a server is never
|
||||
@@ -214,7 +236,8 @@ if (( ! UPGRADE && ! package_trust_refused )); then
|
||||
fi
|
||||
if (( ${#bootstrap[@]} > 0 )); then
|
||||
echo "Installing what the setup questions are built on: ${bootstrap[*]}"
|
||||
sudo dnf install -y "${bootstrap[@]}" >/dev/null || {
|
||||
sudo dnf install -y --repo=fedora --repo=updates \
|
||||
--from-repo=fedora,updates "${bootstrap[@]}" >/dev/null || {
|
||||
echo "Could not install ${bootstrap[*]}, so the setup questions cannot be asked." >&2
|
||||
exit 1
|
||||
}
|
||||
@@ -277,7 +300,12 @@ gsettings set org.gnome.desktop.session idle-delay 0 2>/dev/null || true
|
||||
# is unset and each stage takes the empty-answer path it already documents --
|
||||
# which is why this is a flag rather than a rewrite of seven stage scripts.
|
||||
if (( ! UPGRADE )); then
|
||||
PANAMA_ANSWERS="$(mktemp -t panama-answers.XXXXXX)"
|
||||
PANAMA_ANSWERS="$(mktemp -u -t panama-answers.XXXXXX)" || exit 1
|
||||
umask 077
|
||||
if ! (set -o noclobber; : >"$PANAMA_ANSWERS") 2>/dev/null; then
|
||||
PANAMA_ANSWERS=""
|
||||
exit 1
|
||||
fi
|
||||
export PANAMA_ANSWERS
|
||||
|
||||
if ! PANAMA_ROLE_PRESET="$ROLE_PRESET" "$PANAMA_PATH/setup/scripts/interview"; then
|
||||
@@ -343,7 +371,7 @@ fi
|
||||
if [[ "$PANAMA_ROLE" == server ]]; then
|
||||
STAGES=(install-packages link-dotfiles link-user setup-server link-server setup-identity)
|
||||
else
|
||||
STAGES=(install-packages link-dotfiles link-skills link-user link-mcp change-settings link-vicinae-scripts setup-identity install-hardware)
|
||||
STAGES=(install-packages link-dotfiles link-skills link-user change-settings link-vicinae-scripts setup-identity install-hardware)
|
||||
fi
|
||||
|
||||
# The two an upgrade drops. Both exist only to act on interview answers, and
|
||||
@@ -366,16 +394,11 @@ for stage in "${STAGES[@]}"; do
|
||||
script="$PANAMA_PATH/setup/scripts/$stage"
|
||||
[[ -x "$script" ]] || continue
|
||||
printf '\n=== %s ===\n' "$stage"
|
||||
if (( package_trust_refused )) && stage_opens_dnf "$stage"; then
|
||||
echo "Skipped: the package repository trust check refused package work."
|
||||
continue
|
||||
fi
|
||||
if [[ "$stage" == install-packages ]]; then
|
||||
package_start_hash=""
|
||||
package_state_status=0
|
||||
package_start_hash="$(hash_packages)" || package_state_status=2
|
||||
if (( package_state_status == 0 )); then
|
||||
packages_needed "$package_start_hash" || package_state_status=$?
|
||||
fi
|
||||
packages_needed || package_state_status=$?
|
||||
package_start_hash="$PACKAGE_START_HASH"
|
||||
if (( package_state_status == 1 )); then
|
||||
echo "The package lists have not changed since the last run; skipping."
|
||||
echo "Run with --packages to install them anyway."
|
||||
@@ -395,12 +418,9 @@ for stage in "${STAGES[@]}"; do
|
||||
fi
|
||||
else
|
||||
stage_status=$?
|
||||
# A configuration change between the preflight and this stage. Suppress the
|
||||
# remaining DNF stages, keep the safe ones, and carry the status to the end.
|
||||
if [[ "$stage" == install-packages && "$stage_status" -eq "$TERRA_TRUST_FAILURE_STATUS" ]]; then
|
||||
printf '!!! %s stopped on an untrusted package repository\n' "$stage" >&2
|
||||
package_trust_refused=1
|
||||
continue
|
||||
exit "$stage_status"
|
||||
fi
|
||||
failed+=("$stage")
|
||||
printf '!!! %s failed\n' "$stage" >&2
|
||||
@@ -426,16 +446,8 @@ done
|
||||
# written for -- and baselining would skip every one of them forever. Every
|
||||
# migration is self-guarding and a no-op where it does not apply, so running
|
||||
# them is the safe direction.
|
||||
#
|
||||
# Held back when package work was refused. A migration is free to run a DNF
|
||||
# transaction -- the ChatGPT package replacement does exactly that -- so the
|
||||
# repositories have to be trustworthy before any of them is allowed to run.
|
||||
# They are not marked applied either, so the next run still has them pending.
|
||||
migrate="$PANAMA_PATH/bin/panama-migrate"
|
||||
if (( package_trust_refused )) && [[ -x "$migrate" ]]; then
|
||||
printf '\n=== migrations ===\n'
|
||||
echo "Skipped: the package repository trust check refused package work."
|
||||
elif [[ -x "$migrate" ]]; then
|
||||
if [[ -x "$migrate" ]]; then
|
||||
printf '\n=== migrations ===\n'
|
||||
if (( UPGRADE )) || [[ -d "$STATE_DIR/migrations" ]]; then
|
||||
"$migrate" run || failed+=(migrations)
|
||||
@@ -483,17 +495,6 @@ else
|
||||
retry='./install'
|
||||
fi
|
||||
|
||||
# Reported last and on its own, because it is not an ordinary stage failure:
|
||||
# everything safe did run, and what did not run is named rather than buried in a
|
||||
# list. The exit status stays 78 so a caller can still tell the two apart.
|
||||
if (( package_trust_refused )); then
|
||||
printf 'Package work was refused: the Terra repository configuration on this\n' >&2
|
||||
printf 'machine is not one Panama can verify. Skipped: %s\n' "${DNF_STAGES[*]}" >&2
|
||||
printf 'Everything that touches no repository was still applied.\n' >&2
|
||||
printf 'Inspect it with: panama diagnose\n' >&2
|
||||
exit "$TERRA_TRUST_FAILURE_STATUS"
|
||||
fi
|
||||
|
||||
if (( ${#failed[@]} == 0 )); then
|
||||
if (( UPGRADE )); then
|
||||
echo "Panama is up to date."
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# replace the community ChatGPT Desktop build with the official OpenAI package
|
||||
#
|
||||
# `panama app chatgpt-desktop` used to build a community wrapper (codex-desktop)
|
||||
# from the upstream macOS disk image, complete with a local rebuild daemon.
|
||||
# OpenAI ships an official Linux RPM now, and the installer takes that instead;
|
||||
# this repairs machines still carrying the community build. The official
|
||||
# package goes on before the community one comes off, so a failure part-way
|
||||
# leaves the machine with an app, never without one.
|
||||
#
|
||||
# Rules, because the runner cannot enforce them:
|
||||
#
|
||||
# * Safe to run twice. The marker records success, not intent.
|
||||
# * Tolerant of the repair already being correct -- the user may have fixed
|
||||
# it by hand, or a later ./install may have put it back.
|
||||
# * Root work goes through `panama-sudo --reason "..."`, never bare sudo,
|
||||
# so the password prompt names the repair.
|
||||
# * Exit non-zero to be retried at the next login. Exit zero only when the
|
||||
# machine is genuinely in the state this describes.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||
|
||||
# Machines that never had the community build are already correct. The official
|
||||
# app is the installer's job, not this one's.
|
||||
rpm -q codex-desktop >/dev/null 2>&1 || exit 0
|
||||
|
||||
# The same verified repository the installer establishes: the pinned signing
|
||||
# key, then a repository that names it, so dnf checks OpenAI's signature before
|
||||
# root installs anything. See setup/lib/chatgpt-package.
|
||||
# shellcheck source=../setup/lib/chatgpt-package
|
||||
source "$PANAMA_PATH/setup/lib/chatgpt-package"
|
||||
|
||||
sudo_cmd=(sudo)
|
||||
if [[ -t 0 && -x "$PANAMA_PATH/bin/panama-sudo" ]]; then
|
||||
sudo_cmd=(
|
||||
"$PANAMA_PATH/bin/panama-sudo" --reason
|
||||
"Replacing the community-built ChatGPT Desktop (codex-desktop) with the official OpenAI package"
|
||||
--
|
||||
)
|
||||
fi
|
||||
|
||||
# The official package first, so the machine is never left without one.
|
||||
if ! rpm -q chatgpt >/dev/null 2>&1; then
|
||||
chatgpt_install_repository "${sudo_cmd[@]}"
|
||||
"${sudo_cmd[@]}" dnf install -y chatgpt
|
||||
fi
|
||||
|
||||
# The community package's updater is a user unit; stop it before dnf removes
|
||||
# the unit file out from under it. Removal also takes the app in /opt, both
|
||||
# binaries, and the polkit policy the local rebuilds needed.
|
||||
systemctl --user disable --now codex-update-manager.service 2>/dev/null || true
|
||||
"${sudo_cmd[@]}" dnf remove -y codex-desktop
|
||||
systemctl --user daemon-reload 2>/dev/null || true
|
||||
|
||||
# The rebuild state the updater kept; the official package needs none of it.
|
||||
rm -rf "${XDG_CACHE_HOME:-$HOME/.cache}/codex-update-manager" \
|
||||
"${XDG_CACHE_HOME:-$HOME/.cache}/codex-runtimes" \
|
||||
"${XDG_CONFIG_HOME:-$HOME/.config}/codex-update-manager" \
|
||||
"${XDG_STATE_HOME:-$HOME/.local/state}/codex-update-manager"
|
||||
|
||||
echo "Replaced the community codex-desktop build with the official chatgpt package."
|
||||
+6
-33
@@ -6,34 +6,13 @@ screen, plus rootless podman compose services managed as systemd user units.
|
||||
|
||||
```
|
||||
server/
|
||||
vps/ The catalog panama-server installs from, one directory
|
||||
per service: compose.yml, the podman-<name>.service user
|
||||
unit, and an .env.example naming what the service needs told
|
||||
home/ The home server's services in the same shape (see below)
|
||||
containers/ One directory per service: compose.yml, the
|
||||
podman-<name>.service user unit, and an .env.example
|
||||
naming what the service needs told
|
||||
scripts/ update-containers, the nightly image updater
|
||||
systemd/ The units behind it, linked by setup-server
|
||||
```
|
||||
|
||||
## The home server
|
||||
|
||||
`server/home/` mirrors `~/Server` on the home server, where the services still
|
||||
run under Docker. Each directory has the same three files as the VPS catalog,
|
||||
and the unit is drafted for the eventual move to rootless podman. It has never
|
||||
run, and the composes still use Docker-only features (host networking, the
|
||||
GPU, the Docker socket) that need converting first. Nothing here is
|
||||
installable: panama-server reads `server/vps/` only, so a service moves into
|
||||
the catalog when it actually moves to podman.
|
||||
|
||||
Paths keep their nesting (`server/home/Agents/Gitea` is `~/Server/Agents/Gitea`).
|
||||
Three services are checkouts of upstream repositories. For those, only the
|
||||
local files are kept, and an `UPSTREAM` file names the repository and the exact
|
||||
commit the checkout sits at: `PostHog`, `Sentry` and `Plausible`.
|
||||
|
||||
Both trees are snapshots of the live machines with every secret replaced by a
|
||||
`${VAR}` named in `.env.example`, or by `CHANGE_ME` in files that are not
|
||||
compose files. The live servers still carry some secrets inline, so a live
|
||||
compose and its copy here can differ on those lines alone.
|
||||
|
||||
## The contract with ~/Server
|
||||
|
||||
The repository carries the definitions; the machine carries the state.
|
||||
@@ -56,18 +35,12 @@ is what `panama server enable` refuses to start on.
|
||||
## Conventions
|
||||
|
||||
- Service directories are TitleCase, matching `~/Server` as it has always
|
||||
looked: `server/vps/Gitea` becomes `~/Server/Gitea`.
|
||||
looked: `server/containers/Gitea` becomes `~/Server/Gitea`.
|
||||
- Bind mounts live under `./data/` in new services. Imported services keep
|
||||
the volume path they were running with until their cutover, because the
|
||||
repo copy must render identically to the live one — renames happen with
|
||||
the service stopped, not in git.
|
||||
- Only 80, 443 and 81 are open, and `setup-server` opens each one only where
|
||||
nothing already grants it. A port reachable through an enabled service or an
|
||||
accepting rich rule — a CDN-scoped grant, say — is left exactly as it is,
|
||||
because turning a narrow grant into a public one during an unattended
|
||||
`panama update` is not a decision an installer gets to make. A machine that
|
||||
manages its own firewall opts out for good with
|
||||
`echo manual > ~/.local/state/panama/firewall`. Everything else is
|
||||
- Only 80, 443 and 81 are open (setup-server's doing). Everything else is
|
||||
reverse-proxied over the `nginx-bridge` network by container name, with no
|
||||
published ports of its own. A service that truly needs another host port
|
||||
says so in a README in its directory, and the port is opened by hand.
|
||||
@@ -94,7 +67,7 @@ ones — and verify it came back up. One service at a time, quiet hours.
|
||||
|
||||
## Adding a service
|
||||
|
||||
Add `server/vps/<Name>/` with the three files, modelled on any
|
||||
Add `server/containers/<Name>/` with the three files, modelled on any
|
||||
existing service. The unit is the standard oneshot `podman compose up -d`
|
||||
shape (`WorkingDirectory=%h/Server/<Name>`); the shape contract pins the
|
||||
details. Secrets go in `.env.example` as `KEY=CHANGE_ME` lines.
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -18,8 +18,3 @@ AUTHENTIK_EMAIL__USE_SSL=false
|
||||
AUTHENTIK_EMAIL__TIMEOUT=10
|
||||
# Email address authentik will send from, should have a correct @domain
|
||||
AUTHENTIK_EMAIL__FROM=[email protected]
|
||||
|
||||
# Also set on the live machine
|
||||
AUTHENTIK_IMAGE=CHANGE_ME
|
||||
PG_DB=CHANGE_ME
|
||||
PG_USER=CHANGE_ME
|
||||
File renamed without changes.
+1
@@ -1,4 +1,5 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
Description=Podman Compose: Authentik
|
||||
After=network-online.target podman.socket podman-postgresql.service
|
||||
Wants=network-online.target podman-postgresql.service
|
||||
@@ -35,11 +35,6 @@ services:
|
||||
# No published port: NPM proxies to http://beszel:8090 over nginx-bridge.
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
# Login is SSO-only through Authentik (OIDC provider "Gibs Auth", app slug
|
||||
# beszel). The OAuth config lives in data.db (users collection options),
|
||||
# not here. The PocketBase superuser panel at /_/ keeps its own password,
|
||||
# and is the way back in if Authentik is down: flip this to false there.
|
||||
- DISABLE_PASSWORD_AUTH=true
|
||||
# Lets the hub reach a host-network agent. Under rootless podman this resolves to
|
||||
# the nginx-bridge gateway (172.18.0.1). Add the VPS in the hub UI with host
|
||||
# "host.docker.internal", port 45876.
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
Description=Podman Compose: Beszel
|
||||
After=network-online.target podman.socket
|
||||
Wants=network-online.target
|
||||
File renamed without changes.
@@ -7,7 +7,7 @@
|
||||
# BEFORE FIRST START, create its role and database on the SHARED postgres (this stack
|
||||
# no longer ships its own):
|
||||
# podman exec -i postgresql psql -U npm <<'SQL'
|
||||
# CREATE ROLE calcom LOGIN PASSWORD '617408b6790b546811cc38d0353ea506';
|
||||
# CREATE ROLE calcom LOGIN PASSWORD '<see this service .env on the machine>';
|
||||
# CREATE DATABASE calcom OWNER calcom;
|
||||
# SQL
|
||||
# Then add the same lines to PostgreSQL/initdb/00-roles-and-databases.sql.
|
||||
@@ -52,7 +52,7 @@ services:
|
||||
- EMAIL_SERVER_HOST=CHANGE_ME
|
||||
- EMAIL_SERVER_PORT=587
|
||||
- EMAIL_SERVER_USER=CHANGE_ME
|
||||
- EMAIL_SERVER_PASSWORD=CHANGE_ME
|
||||
- EMAIL_SERVER_PASSWORD=${EMAIL_SERVER_PASSWORD}
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
tty: true
|
||||
File renamed without changes.
@@ -57,8 +57,3 @@ DO_NOT_REQUIRE_SSL=true
|
||||
#ACTIONS_USER_TIMEOUT_SECS=
|
||||
#RUST_LOG=
|
||||
#RUST_BACKTRACE=
|
||||
|
||||
# Also set on the live machine
|
||||
BACKEND_PORT=CHANGE_ME
|
||||
PORT=CHANGE_ME
|
||||
SITE_PROXY_PORT=CHANGE_ME
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -9,7 +9,7 @@
|
||||
# 1. Create its role and database on the SHARED postgres (this stack no longer
|
||||
# ships its own):
|
||||
# podman exec -i postgresql psql -U npm <<'SQL'
|
||||
# CREATE ROLE documenso LOGIN PASSWORD 'e2bf60b34aff8ce259e5ed42da46c9c6';
|
||||
# CREATE ROLE documenso LOGIN PASSWORD '<see this service .env on the machine>';
|
||||
# CREATE DATABASE documenso OWNER documenso;
|
||||
# SQL
|
||||
# Then add the same lines to PostgreSQL/initdb/00-roles-and-databases.sql so a
|
||||
@@ -60,7 +60,7 @@ services:
|
||||
- NEXT_PRIVATE_SMTP_HOST=CHANGE_ME
|
||||
- NEXT_PRIVATE_SMTP_PORT=587
|
||||
- NEXT_PRIVATE_SMTP_USERNAME=CHANGE_ME
|
||||
- NEXT_PRIVATE_SMTP_PASSWORD=CHANGE_ME
|
||||
- NEXT_PRIVATE_SMTP_PASSWORD=${NEXT_PRIVATE_SMTP_PASSWORD}
|
||||
- NEXT_PRIVATE_SMTP_FROM_NAME=Documenso
|
||||
- [email protected]
|
||||
labels:
|
||||
File renamed without changes.
@@ -0,0 +1,25 @@
|
||||
networks:
|
||||
nginx-bridge:
|
||||
external: true
|
||||
services:
|
||||
gitea:
|
||||
image: docker.io/gitea/gitea:latest
|
||||
container_name: gitea
|
||||
hostname: gitea
|
||||
domainname: git.gbrown.org
|
||||
networks:
|
||||
- nginx-bridge
|
||||
ports:
|
||||
- '2222:22'
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- USER_UID=1001
|
||||
- USER_GID=1003
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: 'true'
|
||||
volumes:
|
||||
- ./volume:/data:Z
|
||||
- /home/gib/Media/gitea-packages:/data/gitea/packages:Z
|
||||
- ./ssh:/data/git/.ssh:Z
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
File renamed without changes.
File renamed without changes.
+1
@@ -1,4 +1,5 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
Description=Podman Compose: iSponsorBlockTV
|
||||
After=network-online.target podman.socket
|
||||
Wants=network-online.target
|
||||
@@ -140,6 +140,3 @@ KUBERNETES_AUTO_FETCH_SERVICE_ACCOUNT_TOKEN=false
|
||||
|
||||
# ClickHouse (optional) for audit log storage
|
||||
# CLICKHOUSE_URL=http://infisical:infisical@clickhouse:8123/infisical
|
||||
|
||||
# Also set on the live machine
|
||||
CLIENT_GITLAB_LOGIN_URL=CHANGE_ME
|
||||
File renamed without changes.
+1
@@ -1,4 +1,5 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
Description=Podman Compose: Infisical
|
||||
After=network-online.target podman.socket podman-postgresql.service
|
||||
Wants=network-online.target podman-postgresql.service
|
||||
@@ -62,11 +62,3 @@ POSTGRES_CONTAINER_NAME=lashaddict-postgres
|
||||
POSTGRES_USER=gib
|
||||
POSTGRES_PASSWORD=CHANGE_ME
|
||||
POSTGRES_DB=lashaddict-payload
|
||||
|
||||
# Also set on the live machine
|
||||
NEXT_PUBLIC_POSTHOG_KEY=CHANGE_ME
|
||||
NEXT_PUBLIC_POSTHOG_HOST=CHANGE_ME
|
||||
NEXT_PUBLIC_PLAUSIBLE_SRC=CHANGE_ME
|
||||
BACKEND_PORT=CHANGE_ME
|
||||
PORT=CHANGE_ME
|
||||
SITE_PROXY_PORT=CHANGE_ME
|
||||
@@ -6,12 +6,6 @@ services:
|
||||
image: git.gbrown.org/gib/${NEXT_CONTAINER_NAME}:latest
|
||||
container_name: ${NEXT_CONTAINER_NAME}
|
||||
hostname: ${NEXT_CONTAINER_NAME}
|
||||
# Upstream DNS pinned to Cloudflare: Hetzner resolvers held a stale
|
||||
# NXDOMAIN for api.lashaddictstudio.com for hours after it was created
|
||||
# (2026-09-30). Container names still resolve through aardvark-dns.
|
||||
dns:
|
||||
- 1.1.1.1
|
||||
- 1.0.0.1
|
||||
domainname: ${NEXT_DOMAIN}
|
||||
networks:
|
||||
- ${NETWORK:-nginx-bridge}
|
||||
@@ -35,11 +29,6 @@ services:
|
||||
lashaddict-backend:
|
||||
image: ghcr.io/get-convex/convex-backend:${BACKEND_TAG:-latest}
|
||||
container_name: ${BACKEND_CONTAINER_NAME:-lashaddict-backend}
|
||||
# Same Cloudflare DNS pin as lashaddict-next (see its comment): the backend
|
||||
# fetches its own auth discovery URL, which Hetzner resolvers failed.
|
||||
dns:
|
||||
- 1.1.1.1
|
||||
- 1.0.0.1
|
||||
hostname: ${BACKEND_CONTAINER_NAME:-lashaddict-backend}
|
||||
domainname: ${BACKEND_DOMAIN:-lashaddict.gbrown.org}
|
||||
networks:
|
||||
@@ -69,11 +58,6 @@ services:
|
||||
lashaddict-dashboard:
|
||||
image: ghcr.io/get-convex/convex-dashboard:${DASHBOARD_TAG:-latest}
|
||||
container_name: ${DASHBOARD_CONTAINER_NAME:-lashaddict-dashboard}
|
||||
# Same Cloudflare DNS pin as lashaddict-next (see its comment): the backend
|
||||
# fetches its own auth discovery URL, which Hetzner resolvers failed.
|
||||
dns:
|
||||
- 1.1.1.1
|
||||
- 1.0.0.1
|
||||
hostname: ${DASHBOARD_CONTAINER_NAME:-lashaddict-dashboard}
|
||||
domainname: ${DASHBOARD_DOMAIN:-dashboard.${BACKEND_DOMAIN:-lashaddict.gbrown.org}}
|
||||
networks:
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,4 +1,5 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
Description=Podman Compose: N8n
|
||||
After=network-online.target podman-postgresql.service
|
||||
Wants=network-online.target
|
||||
File renamed without changes.
+1
@@ -37,6 +37,7 @@ services:
|
||||
#networks: ['nginx-bridge']
|
||||
#environment:
|
||||
#POSTGRES_USER: 'npm'
|
||||
#POSTGRES_PASSWORD: '<see this service .env on the machine>'
|
||||
#POSTGRES_DB: 'npm'
|
||||
#labels:
|
||||
#com.centurylinklabs.watchtower.enable: 'true'
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
Description=Podman Compose: Nginx Proxy Manager
|
||||
After=network-online.target podman-postgresql.service
|
||||
Wants=network-online.target
|
||||
@@ -0,0 +1,2 @@
|
||||
PENPOT_SECRET_KEY=CHANGE_ME
|
||||
PENPOT_DATABASE_PASSWORD=CHANGE_ME
|
||||
@@ -0,0 +1,120 @@
|
||||
# Penpot — VPS (ROOTLESS PODMAN). Ported from the home server 2026-08-12.
|
||||
#
|
||||
# ⚠️ NOT STARTED. No systemd unit is enabled. It has never run on either machine, so
|
||||
# there is no data to migrate. Bring it up with:
|
||||
# systemctl --user enable --now podman-penpot.service
|
||||
#
|
||||
# BEFORE FIRST START, create its role and database on the SHARED postgres (this stack
|
||||
# no longer ships its own):
|
||||
# podman exec -i postgresql psql -U npm <<'SQL'
|
||||
# CREATE ROLE penpot LOGIN PASSWORD '<see this service .env on the machine>';
|
||||
# CREATE DATABASE penpot OWNER penpot;
|
||||
# SQL
|
||||
# Then add the same lines to PostgreSQL/initdb/00-roles-and-databases.sql.
|
||||
#
|
||||
# NOTE: the home version used postgres:15-alpine with --data-checksums. The shared
|
||||
# instance is postgres 17 without checksums. Penpot does not care, and since there is
|
||||
# no existing data this is not a migration — but if you ever DO need checksums, they
|
||||
# can only be set at initdb time for the whole cluster.
|
||||
#
|
||||
# Redis stays local to this stack: Penpot uses it for transient session and
|
||||
# notification state only, so it is not worth centralising.
|
||||
#
|
||||
# Rootless adaptations from the home version:
|
||||
# - Dropped its private postgres:15-alpine; uses the shared instance (AGENTS.md §2).
|
||||
# - PENPOT_DATABASE_URI repointed from penpot-db to the shared postgresql.
|
||||
# - :Z on the assets bind mount. It is shared between frontend and backend, so it
|
||||
# uses LOWERCASE :z -- uppercase would give each container a private MCS category
|
||||
# and the second to start would lock the first out (AGENTS.md §6).
|
||||
# - /etc/localtime mounts removed in favour of TZ (AGENTS.md §8).
|
||||
#
|
||||
# Registration is disabled by default (PENPOT_FLAGS disable-registration), so create
|
||||
# the first account from the backend container before you can log in.
|
||||
#
|
||||
# NPM: proxy penpot.gbrown.org -> http://penpot-frontend:8080
|
||||
|
||||
networks:
|
||||
nginx-bridge:
|
||||
external: true
|
||||
|
||||
services:
|
||||
penpot-frontend:
|
||||
image: docker.io/penpotapp/frontend:latest
|
||||
container_name: penpot-frontend
|
||||
hostname: penpot-frontend
|
||||
domainname: penpot.gbrown.org
|
||||
networks: ['nginx-bridge']
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- PENPOT_FLAGS=disable-registration enable-login-with-password disable-smtp
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
volumes:
|
||||
- ./volumes/assets:/opt/data/assets:z
|
||||
depends_on:
|
||||
- penpot-backend
|
||||
- penpot-exporter
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
|
||||
penpot-backend:
|
||||
image: docker.io/penpotapp/backend:latest
|
||||
container_name: penpot-backend
|
||||
hostname: penpot-backend
|
||||
domainname: penpot.gbrown.org
|
||||
networks: ['nginx-bridge']
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- PENPOT_FLAGS=disable-registration enable-login-with-password disable-smtp enable-prepl-server
|
||||
- PENPOT_PUBLIC_URI=https://penpot.gbrown.org
|
||||
- PENPOT_SECRET_KEY=${PENPOT_SECRET_KEY}
|
||||
# Shared postgres, not a private container
|
||||
- PENPOT_DATABASE_URI=postgresql://postgresql/penpot
|
||||
- PENPOT_DATABASE_USERNAME=penpot
|
||||
- PENPOT_DATABASE_PASSWORD=${PENPOT_DATABASE_PASSWORD}
|
||||
- PENPOT_REDIS_URI=redis://penpot-redis/0
|
||||
- PENPOT_ASSETS_STORAGE_BACKEND=assets-fs
|
||||
- PENPOT_STORAGE_ASSETS_FS_DIRECTORY=/opt/data/assets
|
||||
- PENPOT_TELEMETRY_ENABLED=false
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
volumes:
|
||||
- ./volumes/assets:/opt/data/assets:z
|
||||
depends_on:
|
||||
- penpot-redis
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
|
||||
penpot-exporter:
|
||||
image: docker.io/penpotapp/exporter:latest
|
||||
container_name: penpot-exporter
|
||||
hostname: penpot-exporter
|
||||
domainname: penpot.gbrown.org
|
||||
networks: ['nginx-bridge']
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- PENPOT_PUBLIC_URI=http://penpot-frontend:8080
|
||||
- PENPOT_REDIS_URI=redis://penpot-redis/0
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
depends_on:
|
||||
- penpot-redis
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
|
||||
penpot-redis:
|
||||
image: docker.io/library/redis:7-alpine
|
||||
container_name: penpot-redis
|
||||
hostname: penpot-redis
|
||||
domainname: penpot.gbrown.org
|
||||
networks: ['nginx-bridge']
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
File renamed without changes.
File renamed without changes.
+1
@@ -1,4 +1,5 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
Description=Podman Compose: Portainer
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
File renamed without changes.
File renamed without changes.
+1
@@ -1,4 +1,5 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
Description=Podman Compose: PostgreSQL
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
@@ -0,0 +1 @@
|
||||
RUSTDESK_API_RUSTDESK_JWT_KEY=CHANGE_ME
|
||||
@@ -72,7 +72,7 @@ services:
|
||||
- RUSTDESK_API_RUSTDESK_ID_SERVER=relay.gbrown.org
|
||||
- RUSTDESK_API_RUSTDESK_RELAY_SERVER=relay.gbrown.org
|
||||
- RUSTDESK_API_RUSTDESK_API_SERVER=https://rustdesk.gbrown.org
|
||||
- RUSTDESK_API_RUSTDESK_KEY=${RUSTDESK_API_RUSTDESK_KEY}
|
||||
- RUSTDESK_API_RUSTDESK_KEY=WLgvHhau6aa5nDPQutTHeQBpIrOEb8aPXByVBWQwkKc=
|
||||
- RUSTDESK_API_RUSTDESK_JWT_KEY=${RUSTDESK_API_RUSTDESK_JWT_KEY}
|
||||
- RUSTDESK_API_LANG=en
|
||||
- RUSTDESK_API_APP_DISABLE_PWD_LOGIN=true
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
Description=Podman Compose: Rustdesk
|
||||
After=network-online.target podman.socket
|
||||
Wants=network-online.target
|
||||
@@ -64,6 +64,3 @@ SPOON_AGENT_WORKER_ID=production-worker
|
||||
SPOON_AGENT_WORKER_HTTP_PORT="3921"
|
||||
SPOON_AGENT_WORKER_INTERNAL_TOKEN=CHANGE_ME
|
||||
SPOON_AGENT_WORKER_URL=http://spoon-agent-worker:3921
|
||||
|
||||
# Also set on the live machine
|
||||
PORT=CHANGE_ME
|
||||
File renamed without changes.
File renamed without changes.
@@ -56,8 +56,3 @@ POSTGRES_URL=CHANGE_ME
|
||||
#ACTIONS_USER_TIMEOUT_SECS=
|
||||
#RUST_LOG=
|
||||
#RUST_BACKTRACE=
|
||||
|
||||
# Also set on the live machine
|
||||
BACKEND_PORT=CHANGE_ME
|
||||
PORT=CHANGE_ME
|
||||
SITE_PROXY_PORT=CHANGE_ME
|
||||
File renamed without changes.
File renamed without changes.
@@ -19,7 +19,7 @@ services:
|
||||
container_name: convertx
|
||||
hostname: convertx
|
||||
domainname: convert.tools.gbrown.org
|
||||
networks: ["nginx-bridge"]
|
||||
networks: ['nginx-bridge']
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- ALLOW_UNAUTHENTICATED=true
|
||||
@@ -37,7 +37,7 @@ services:
|
||||
container_name: it-tools
|
||||
hostname: it-tools
|
||||
domainname: tools.gbrown.org
|
||||
networks: ["nginx-bridge"]
|
||||
networks: ['nginx-bridge']
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
labels:
|
||||
@@ -45,12 +45,33 @@ services:
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
|
||||
pdf-tools:
|
||||
image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest
|
||||
container_name: pdf-tools
|
||||
hostname: pdf-tools
|
||||
domainname: pdf.tools.gbrown.org
|
||||
networks: ['nginx-bridge']
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- LANGS=en_US
|
||||
- DOCKER_ENABLE_SECURITY=true
|
||||
volumes:
|
||||
- ./volumes/pdf-tools/trainingData:/usr/share/tessdata:Z
|
||||
- ./volumes/pdf-tools/extraConfigs:/configs:Z
|
||||
- ./volumes/pdf-tools/customFiles:/customFiles:Z
|
||||
- ./volumes/pdf-tools/logs:/logs/:Z
|
||||
- ./volumes/pdf-tools/pipeline:/pipeline/:Z
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
|
||||
bento-pdf:
|
||||
image: ghcr.io/alam00000/bentopdf:latest
|
||||
container_name: bento-pdf
|
||||
hostname: bento-pdf
|
||||
domainname: bento.tools.gbrown.org
|
||||
networks: ["nginx-bridge"]
|
||||
networks: ['nginx-bridge']
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- PUID=1000
|
||||
@@ -61,28 +82,7 @@ services:
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080"]
|
||||
test: ['CMD', 'wget', '--spider', '-q', 'http://localhost:8080']
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
# pdf-tools:
|
||||
# image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest
|
||||
# container_name: pdf-tools
|
||||
# hostname: pdf-tools
|
||||
# domainname: pdf.tools.gbrown.org
|
||||
# networks: ["nginx-bridge"]
|
||||
# environment:
|
||||
# - TZ=America/New_York
|
||||
# - LANGS=en_US
|
||||
# - DOCKER_ENABLE_SECURITY=true
|
||||
# volumes:
|
||||
# - ./volumes/pdf-tools/trainingData:/usr/share/tessdata:Z
|
||||
# - ./volumes/pdf-tools/extraConfigs:/configs:Z
|
||||
# - ./volumes/pdf-tools/customFiles:/customFiles:Z
|
||||
# - ./volumes/pdf-tools/logs:/logs/:Z
|
||||
# - ./volumes/pdf-tools/pipeline:/pipeline/:Z
|
||||
# labels:
|
||||
# com.centurylinklabs.watchtower.enable: "true"
|
||||
# tty: true
|
||||
# restart: unless-stopped
|
||||
@@ -1,4 +1,5 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
Description=Podman Compose: Tools
|
||||
After=network-online.target podman.socket
|
||||
Wants=network-online.target
|
||||
File renamed without changes.
+1
@@ -1,4 +1,5 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
Description=Podman Compose: Uptime
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
@@ -20,7 +20,3 @@ SMTP_HOST=smtp.usesend.gbrown.org
|
||||
SMTP_USER=usesend
|
||||
USESEND_PORT=3000
|
||||
USESEND_URL=https://usesend.gbrown.org
|
||||
|
||||
# Also set on the live machine
|
||||
AWS_ACCESS_KEY_ID=CHANGE_ME
|
||||
AWS_SECRET_ACCESS_KEY=CHANGE_ME
|
||||
@@ -0,0 +1,88 @@
|
||||
name: usesend
|
||||
networks:
|
||||
nginx-bridge:
|
||||
external: true
|
||||
services:
|
||||
redis:
|
||||
image: redis:7
|
||||
container_name: usesend-redis
|
||||
hostname: usesend-redis
|
||||
networks:
|
||||
- nginx-bridge
|
||||
env_file: .env
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: 'true'
|
||||
volumes:
|
||||
- ./volumes/redis:/data:Z
|
||||
command:
|
||||
- redis-server
|
||||
- --maxmemory-policy
|
||||
- noeviction
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
minio:
|
||||
image: minio/minio
|
||||
container_name: usesend-storage
|
||||
hostname: usesend-storage
|
||||
networks:
|
||||
- nginx-bridge
|
||||
env_file: .env
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: 'true'
|
||||
volumes:
|
||||
- ./volumes/minio:/data:Z
|
||||
entrypoint: sh
|
||||
command: -c 'mkdir -p /data/unsend && minio server /data --console-address ':9001' --address ':9002''
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
smtp-server:
|
||||
image: usesend/smtp-proxy:v1.6.6
|
||||
container_name: usesend-smtp-server
|
||||
hostname: usesend-smtp
|
||||
networks:
|
||||
- nginx-bridge
|
||||
env_file: .env
|
||||
environment:
|
||||
- SMTP_AUTH_USERNAME=${SMTP_USER:?err}
|
||||
- USESEND_BASE_URL=${USESEND_URL:?err}
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: 'true'
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
usesend:
|
||||
image: usesend/usesend:v1.6.6
|
||||
container_name: usesend
|
||||
hostname: usesend
|
||||
domainname: ${USESEND_URL:?err}
|
||||
networks:
|
||||
- nginx-bridge
|
||||
env_file: .env
|
||||
environment:
|
||||
- PORT=${USESEND_PORT:-3000}
|
||||
- DATABASE_URL=${DATABASE_URL:?err}
|
||||
- NEXTAUTH_URL=${USESEND_URL:?err}
|
||||
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET:?err}
|
||||
- AWS_ACCESS_KEY=${AWS_ACCESS_KEY:?err}
|
||||
- AWS_SECRET_KEY=${AWS_SECRET_KEY:?err}
|
||||
- AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION:?err}
|
||||
- GITHUB_ID=${GITHUB_ID:?err}
|
||||
- GITHUB_SECRET=${GITHUB_SECRET:?err}
|
||||
- REDIS_URL=${REDIS_URL:?err}
|
||||
- NEXT_PUBLIC_IS_CLOUD=${NEXT_PUBLIC_IS_CLOUD:-false}
|
||||
- API_RATE_LIMIT=${API_RATE_LIMIT:-1}
|
||||
- SMTP_HOST=${SMTP_HOST:-smtp.usesend.gbrown.org}
|
||||
- SMTP_USER=${SMTP_USER:-usesend}
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: 'true'
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_started
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
File renamed without changes.
@@ -0,0 +1 @@
|
||||
ADMIN_TOKEN=CHANGE_ME
|
||||
@@ -0,0 +1,55 @@
|
||||
# Vaultwarden — VPS (ROOTLESS PODMAN) port of the home server's ~/Server/vaultwarden,
|
||||
# staged 2026-08-12.
|
||||
#
|
||||
# ⚠️ STAGED, NOT AUTHORITATIVE. The home instance is still live and still serving
|
||||
# vault.gbrown.org. This copy holds a point-in-time snapshot of the SQLite database.
|
||||
# DO NOT log into this one and add credentials before cutover -- the two would diverge
|
||||
# and one side's changes would be lost.
|
||||
#
|
||||
# Cutover procedure (do it in this order, it matters):
|
||||
# 1. stop vaultwarden on the home server (clean SQLite, no torn WAL)
|
||||
# 2. rsync -a server.gib:~/Server/vaultwarden/volume/ ~/Server/Vaultwarden/volume/
|
||||
# 3. systemctl --user restart podman-vaultwarden
|
||||
# 4. point vault.gbrown.org at the VPS, add the NPM proxy host
|
||||
# 5. verify a login + an item decrypts, THEN leave home stopped
|
||||
#
|
||||
# The SQLite copy taken while home was running is crash-consistent only. Step 1 is what
|
||||
# makes it clean -- don't skip it.
|
||||
#
|
||||
# Rootless adaptations:
|
||||
# - :Z on the data mount (SELinux Enforcing).
|
||||
# - /etc/localtime mount dropped in favour of TZ (AGENTS.md §8).
|
||||
# - No published port; NPM proxies to http://vaultwarden:80 over nginx-bridge.
|
||||
# - Image starts as root and drops privileges internally, so no userns_mode needed.
|
||||
#
|
||||
# NOTE: ADMIN_TOKEN is inline here, matching the home server's file verbatim. It grants
|
||||
# access to /admin. Worth moving to a mode-600 .env at some point -- compose files get
|
||||
# rsynced offsite by ~/Server/backup.
|
||||
|
||||
networks:
|
||||
nginx-bridge:
|
||||
external: true
|
||||
|
||||
services:
|
||||
vaultwarden:
|
||||
image: docker.io/vaultwarden/server:latest
|
||||
container_name: vaultwarden
|
||||
hostname: vaultwarden
|
||||
domainname: vault.gbrown.org
|
||||
networks:
|
||||
- nginx-bridge
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- DOMAIN=https://vault.gbrown.org
|
||||
- LOGIN_RATELIMIT_MAX_BURST=10
|
||||
- LOGIN_RATELIMIT_SECONDS=60
|
||||
- WEB_VAULT_ENABLED=true
|
||||
- SIGNUPS_ALLOWED=false
|
||||
- WEBSOCKET_ENABLED=true
|
||||
- ADMIN_TOKEN=${ADMIN_TOKEN}
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
volumes:
|
||||
- ./volume:/data:Z
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
[Unit]
|
||||
RequiresMountsFor=/home/gib/Media
|
||||
Description=Podman Compose: Vaultwarden
|
||||
After=network-online.target podman.socket
|
||||
Wants=network-online.target
|
||||
@@ -1,48 +0,0 @@
|
||||
# Beszel AGENT ONLY — the hub now lives on the VPS.
|
||||
#
|
||||
# The hub (web UI + database) was moved to the VPS on 2026-08-11 and is reachable at
|
||||
# https://beszel.gibbyb.com. This host keeps only the agent, which collects metrics and
|
||||
# serves them to that hub over the WireGuard link (VPS 192.168.2.2 -> here 192.168.1.2:45876).
|
||||
#
|
||||
# KEY below is the VPS hub's public key. If the VPS hub is ever rebuilt its keypair
|
||||
# changes, and this value must be updated to match:
|
||||
# ssh-keygen -y -f ~/Server/Beszel/volumes/hub/id_ed25519 (run on the VPS)
|
||||
#
|
||||
# Previous config, including the local hub service, is in compose.yml.bak-2026-08-11.
|
||||
#
|
||||
# The agent uses network_mode: host — that is the documented setup and is what lets it
|
||||
# read real host network interfaces rather than a container's veth. Docker forbids setting
|
||||
# hostname/domainname together with host networking, so those keys are absent.
|
||||
|
||||
services:
|
||||
beszel-agent:
|
||||
image: henrygd/beszel-agent-nvidia:latest
|
||||
container_name: beszel-agent
|
||||
network_mode: host
|
||||
# Give the agent read access to NVIDIA's management interface so Beszel can
|
||||
# report GPU utilization, temperature, power draw, and VRAM usage.
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
capabilities: [utility]
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- PORT=45876
|
||||
- KEY=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDBWNqxKGRltqXJZg+wlDOtICeLo19ftQ6P3P8+uJg8y
|
||||
# /boot is a separate 974M partition (nvme1n1p1), not part of the LUKS
|
||||
# root the agent finds on its own. It filling up is what broke a 1032
|
||||
# package dnf transaction on 2026-08-09, so it is worth alerting on.
|
||||
- EXTRA_FILESYSTEMS=/boot
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
volumes:
|
||||
- ./volumes/agent:/var/lib/beszel-agent
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- /boot:/boot:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
@@ -1,18 +0,0 @@
|
||||
[Unit]
|
||||
Description=Podman Compose: Agents/Beszel
|
||||
After=network-online.target podman.socket
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=%h/Server/Agents/Beszel
|
||||
ExecStart=/usr/bin/podman compose up -d
|
||||
ExecStop=/usr/bin/podman compose down
|
||||
RemainAfterExit=yes
|
||||
TimeoutStartSec=900
|
||||
TimeoutStopSec=90
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -1 +0,0 @@
|
||||
GITEA_RUNNER_REGISTRATION_TOKEN=CHANGE_ME
|
||||
@@ -1,153 +0,0 @@
|
||||
# Gitea Actions runners — 3 parallel runners, each with its own dind sidecar.
|
||||
#
|
||||
# Each runner gets an isolated Docker daemon rather than the host socket, so a
|
||||
# CI job cannot reach the other ~180 containers on this box. Cost: each dind
|
||||
# keeps its own image cache under ./volumes/runner-N/dind.
|
||||
#
|
||||
# The pre-existing host runner (/home/gib/.local/bin/act_runner, name
|
||||
# "server-gib") is intentionally left running.
|
||||
#
|
||||
# Registration token generated with:
|
||||
# docker exec -u git gitea gitea actions generate-runner-token
|
||||
# It is reusable — all three runners register with the same one.
|
||||
|
||||
networks:
|
||||
nginx-bridge:
|
||||
external: true
|
||||
|
||||
x-runner-env: &runner-env
|
||||
TZ: America/New_York
|
||||
GITEA_INSTANCE_URL: https://git.gbrown.org
|
||||
GITEA_RUNNER_REGISTRATION_TOKEN: ${GITEA_RUNNER_REGISTRATION_TOKEN}
|
||||
GITEA_RUNNER_LABELS: "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest,ubuntu-24.04:docker://docker.gitea.com/runner-images:ubuntu-24.04,ubuntu-22.04:docker://docker.gitea.com/runner-images:ubuntu-22.04"
|
||||
DOCKER_TLS_VERIFY: "1"
|
||||
DOCKER_CERT_PATH: /certs/client
|
||||
CONFIG_FILE: /data/config.yml
|
||||
|
||||
x-dind-env: &dind-env
|
||||
TZ: America/New_York
|
||||
DOCKER_TLS_CERTDIR: /certs
|
||||
|
||||
services:
|
||||
gitea-runner-1:
|
||||
image: gitea/act_runner:latest
|
||||
container_name: gitea-runner-1
|
||||
hostname: gitea-runner-1
|
||||
domainname: git.gbrown.org
|
||||
networks: ['nginx-bridge']
|
||||
environment:
|
||||
<<: *runner-env
|
||||
GITEA_RUNNER_NAME: gitea-runner-1
|
||||
DOCKER_HOST: tcp://gitea-runner-1-dind:2376
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
volumes:
|
||||
- ./volumes/runner-1/data:/data
|
||||
- ./volumes/runner-1/certs:/certs
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
depends_on:
|
||||
- gitea-runner-1-dind
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
|
||||
gitea-runner-1-dind:
|
||||
image: docker:dind
|
||||
container_name: gitea-runner-1-dind
|
||||
hostname: gitea-runner-1-dind
|
||||
domainname: git.gbrown.org
|
||||
networks: ['nginx-bridge']
|
||||
privileged: true
|
||||
environment:
|
||||
<<: *dind-env
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
volumes:
|
||||
- ./volumes/runner-1/certs:/certs
|
||||
- ./volumes/runner-1/dind:/var/lib/docker
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
|
||||
gitea-runner-2:
|
||||
image: gitea/act_runner:latest
|
||||
container_name: gitea-runner-2
|
||||
hostname: gitea-runner-2
|
||||
domainname: git.gbrown.org
|
||||
networks: ['nginx-bridge']
|
||||
environment:
|
||||
<<: *runner-env
|
||||
GITEA_RUNNER_NAME: gitea-runner-2
|
||||
DOCKER_HOST: tcp://gitea-runner-2-dind:2376
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
volumes:
|
||||
- ./volumes/runner-2/data:/data
|
||||
- ./volumes/runner-2/certs:/certs
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
depends_on:
|
||||
- gitea-runner-2-dind
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
|
||||
gitea-runner-2-dind:
|
||||
image: docker:dind
|
||||
container_name: gitea-runner-2-dind
|
||||
hostname: gitea-runner-2-dind
|
||||
domainname: git.gbrown.org
|
||||
networks: ['nginx-bridge']
|
||||
privileged: true
|
||||
environment:
|
||||
<<: *dind-env
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
volumes:
|
||||
- ./volumes/runner-2/certs:/certs
|
||||
- ./volumes/runner-2/dind:/var/lib/docker
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
|
||||
gitea-runner-3:
|
||||
image: gitea/act_runner:latest
|
||||
container_name: gitea-runner-3
|
||||
hostname: gitea-runner-3
|
||||
domainname: git.gbrown.org
|
||||
networks: ['nginx-bridge']
|
||||
environment:
|
||||
<<: *runner-env
|
||||
GITEA_RUNNER_NAME: gitea-runner-3
|
||||
DOCKER_HOST: tcp://gitea-runner-3-dind:2376
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
volumes:
|
||||
- ./volumes/runner-3/data:/data
|
||||
- ./volumes/runner-3/certs:/certs
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
depends_on:
|
||||
- gitea-runner-3-dind
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
|
||||
gitea-runner-3-dind:
|
||||
image: docker:dind
|
||||
container_name: gitea-runner-3-dind
|
||||
hostname: gitea-runner-3-dind
|
||||
domainname: git.gbrown.org
|
||||
networks: ['nginx-bridge']
|
||||
privileged: true
|
||||
environment:
|
||||
<<: *dind-env
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
volumes:
|
||||
- ./volumes/runner-3/certs:/certs
|
||||
- ./volumes/runner-3/dind:/var/lib/docker
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
@@ -1,18 +0,0 @@
|
||||
[Unit]
|
||||
Description=Podman Compose: Agents/Gitea
|
||||
After=network-online.target podman.socket
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=%h/Server/Agents/Gitea
|
||||
ExecStart=/usr/bin/podman compose up -d
|
||||
ExecStop=/usr/bin/podman compose down
|
||||
RemainAfterExit=yes
|
||||
TimeoutStartSec=900
|
||||
TimeoutStopSec=90
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -1,39 +0,0 @@
|
||||
services:
|
||||
# portainer:
|
||||
# image: portainer/portainer-ee:latest
|
||||
# container_name: portainer
|
||||
# hostname: portainer
|
||||
# domainname: port.gbrown.org
|
||||
# networks:
|
||||
# - nginx-bridge
|
||||
# labels:
|
||||
# com.centurylinklabs.watchtower.enable: "true"
|
||||
# ports:
|
||||
# - 9443:9443
|
||||
# volumes:
|
||||
# - ./volumes/data:/data
|
||||
# - /var/run/docker.sock:/var/run/docker.sock
|
||||
# tty: true
|
||||
# stdin_open: true
|
||||
# restart: unless-stopped
|
||||
|
||||
portainer-agent:
|
||||
image: portainer/agent:2.33.2
|
||||
container_name: portainer-agent
|
||||
hostname: portainer-agent
|
||||
networks:
|
||||
- nginx-bridge
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
ports: ["9001:9001"]
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /var/lib/docker/volumes:/var/lib/docker/volumes
|
||||
- /:/host
|
||||
tty: true
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
nginx-bridge:
|
||||
external: true
|
||||
@@ -1,18 +0,0 @@
|
||||
[Unit]
|
||||
Description=Podman Compose: Agents/Portainer
|
||||
After=network-online.target podman.socket
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=%h/Server/Agents/Portainer
|
||||
ExecStart=/usr/bin/podman compose up -d
|
||||
ExecStop=/usr/bin/podman compose down
|
||||
RemainAfterExit=yes
|
||||
TimeoutStartSec=900
|
||||
TimeoutStopSec=90
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -1,2 +0,0 @@
|
||||
RTSP_PASSWORD=CHANGE_ME
|
||||
PLUS_API_KEY=CHANGE_ME
|
||||
@@ -1,79 +0,0 @@
|
||||
networks:
|
||||
nginx-bridge:
|
||||
external: true
|
||||
services:
|
||||
frigate:
|
||||
image: ghcr.io/blakeblackshear/frigate:0.17.1-tensorrt # For Nvidia GPU
|
||||
container_name: frigate
|
||||
hostname: frigate
|
||||
domainname: frigate.gbrown.org
|
||||
networks: [nginx-bridge]
|
||||
ports:
|
||||
#- "8971:8971"
|
||||
- "5000:5000"
|
||||
#- "8554:8554"
|
||||
#- "8555:8555"
|
||||
# Check host system's actual render/video/plugdev group IDs with 'getent group render', 'getent group video', and 'getent group plugdev'
|
||||
# Must add these exact IDs in container's group_add section or OpenVINO GPU acceleration will fail
|
||||
group_add:
|
||||
- "105" # render
|
||||
- "110" # render
|
||||
- "39" # video
|
||||
- "46" # plugdev
|
||||
shm_size: "256mb"
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- PGID=1000
|
||||
- PUID=1000
|
||||
- FRIGATE_RTSP_PASSWORD=${RTSP_PASSWORD}
|
||||
#- HSA_OVERRIDE_GFX_VERSION=11.0.0
|
||||
- PLUS_API_KEY=${PLUS_API_KEY}
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
volumes:
|
||||
- ./volumes/frigate/config:/config:rw
|
||||
- ./volumes/frigate/data:/usr/local/nginx/conf:rw
|
||||
- /home/gib/Public/Server_Backup/frigate:/media/frigate
|
||||
- type: tmpfs
|
||||
target: /tmp/cache
|
||||
tmpfs:
|
||||
size: 1000000000
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
# - /dev/bus/usb:/dev/bus/usb # Uncomment for Google Coral USB
|
||||
devices:
|
||||
- /dev/dri:/dev/dri # for intel hwaccel, needs to be updated for your hardware
|
||||
#- /dev/kfd:/dev/kfd
|
||||
stop_grace_period: 30s
|
||||
restart: unless-stopped
|
||||
tty: true
|
||||
security_opt: ['seccomp:unconfined','label:disable','systempaths=unconfined']
|
||||
#privileged: true
|
||||
runtime: nvidia
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: 1
|
||||
capabilities: [gpu]
|
||||
|
||||
mqtt:
|
||||
image: eclipse-mosquitto:2
|
||||
container_name: mqtt
|
||||
hostname: mqtt
|
||||
domainname: mqtt.gbrown.org
|
||||
networks: [nginx-bridge]
|
||||
ports: ["1883:1883"]
|
||||
#env_file: [.env]
|
||||
environment: [TZ=America/New_York]
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
volumes:
|
||||
- ./volumes/mosquitto/config:/mosquitto/config:rw
|
||||
- ./volumes/mosquitto/data:/mosquitto/data:rw
|
||||
- ./volumes/mosquitto/log:/mosquitto/log:rw
|
||||
tty: true
|
||||
security_opt: ['seccomp:unconfined','label:disable','systempaths=unconfined']
|
||||
restart: unless-stopped
|
||||
@@ -1,18 +0,0 @@
|
||||
[Unit]
|
||||
Description=Podman Compose: Frigate
|
||||
After=network-online.target podman.socket
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=%h/Server/Frigate
|
||||
ExecStart=/usr/bin/podman compose up -d
|
||||
ExecStop=/usr/bin/podman compose down
|
||||
RemainAfterExit=yes
|
||||
TimeoutStartSec=900
|
||||
TimeoutStopSec=90
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
Loaded 100 of 227 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user