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
|
||||
|
||||
193 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=()
|
||||
|
||||
+5
-40
@@ -10,16 +10,9 @@
|
||||
# start time, so choosing an agent in Settings takes effect on the next crash
|
||||
# without restarting anything:
|
||||
#
|
||||
# preferredAgent none | claude | codex | t3 ("none" is the default: silence)
|
||||
# preferredAgent none | claude | codex ("none" is the default: silence)
|
||||
# agentAutoApprove true -> the agent starts in its own "don't stop to ask"
|
||||
# mode; false -> its normal prompting mode, untouched.
|
||||
# T3 Code ignores it: a T3 thread runs in the mode its
|
||||
# project is set to, which is a choice made inside T3.
|
||||
#
|
||||
# claude and codex open in a kitty window. t3 starts a thread in the Panama
|
||||
# project on the running T3 Code server and brings it up in the desktop app,
|
||||
# so the investigation lives beside every other thread instead of in a
|
||||
# terminal of its own.
|
||||
#
|
||||
# "none" exits 0 without a word. It is not an error to have no agent; it is the
|
||||
# shipped state, and a rung that shouted about it would be a rung that gets
|
||||
@@ -102,9 +95,8 @@ usage() {
|
||||
cat <<'EOF'
|
||||
Usage: panama-agent [--prompt "text"]
|
||||
|
||||
Opens the agent named by preferredAgent in the Panama checkout: a terminal for
|
||||
claude and codex, a thread in the T3 Code app for t3. With no agent chosen,
|
||||
exits silently: choose one on Settings > System > Agents.
|
||||
Opens the agent named by preferredAgent in a terminal, in the Panama checkout.
|
||||
With no agent chosen, exits silently: choose one on Settings > System > Agents.
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -147,7 +139,7 @@ agent="$(read_setting preferredAgent none)"
|
||||
auto_approve="$(read_setting agentAutoApprove true)"
|
||||
|
||||
case "$agent" in
|
||||
claude | codex | t3) ;;
|
||||
claude | codex) ;;
|
||||
*)
|
||||
printf 'panama-agent: unsupported preferredAgent: %s\n' "$agent" >&2
|
||||
printf 'Choose one on Settings > System > Agents.\n' >&2
|
||||
@@ -156,8 +148,7 @@ case "$agent" in
|
||||
esac
|
||||
|
||||
# Resolved to a path before argv is built, so kitty is never asked to repeat a
|
||||
# PATH lookup this script has already done more carefully than kitty could. T3
|
||||
# Code's CLI is `t3`, which the desktop app writes into ~/.local/bin.
|
||||
# PATH lookup this script has already done more carefully than kitty could.
|
||||
if ! agent_bin="$(resolve_agent "$agent")"; then
|
||||
printf 'panama-agent: %s is not installed.\n' "$agent" >&2
|
||||
printf 'Looked on PATH and in %s.\n' "$HOME/.local/bin" >&2
|
||||
@@ -165,32 +156,6 @@ if ! agent_bin="$(resolve_agent "$agent")"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# T3 Code: no terminal. The thread starts on the running server in the
|
||||
# project whose root is the checkout, with the project's default model, and
|
||||
# the t3code:// link opens it in the app. `project add` refuses a root that is
|
||||
# already a project, which is the common case, so its failure is not one. The
|
||||
# prompt goes over stdin ("-"), so a prompt that begins with a dash is never
|
||||
# read as a flag. With no prompt there is nothing to start, so the app just
|
||||
# opens on the project.
|
||||
if [[ "$agent" == "t3" ]]; then
|
||||
repair_path
|
||||
"$agent_bin" project add "$PANAMA_PATH" >/dev/null 2>&1 || true
|
||||
if [[ -z "$prompt" ]]; then
|
||||
exec setsid "$agent_bin" app "$PANAMA_PATH"
|
||||
fi
|
||||
if ! thread="$(printf '%s' "$prompt" | "$agent_bin" thread start "$PANAMA_PATH" -)"; then
|
||||
printf 'panama-agent: T3 Code could not start a thread.\n' >&2
|
||||
printf 'Is the T3 Code server running? Check: systemctl --user status t3code\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
environment_file="${T3CODE_HOME:-$HOME/.t3}/userdata/environment-id"
|
||||
environment="$(cat "$environment_file" 2>/dev/null || true)"
|
||||
if [[ -n "$environment" ]]; then
|
||||
exec setsid xdg-open "t3code://threads/$environment/$thread"
|
||||
fi
|
||||
exec setsid "$agent_bin" app "$PANAMA_PATH"
|
||||
fi
|
||||
|
||||
# The per-agent launch table. VERIFIED against the installed binaries' --help on
|
||||
# 2026-08-25 (claude 2.1.245, codex-cli 0.149.1) -- these flags are not the same
|
||||
# from release to release, so re-run --help before changing them.
|
||||
|
||||
+18
-68
@@ -2,12 +2,10 @@
|
||||
#
|
||||
# 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. A service may also carry helper units (any
|
||||
# other *.service or *.timer, e.g. a cert refresh) and helper files (scripts,
|
||||
# a README) beside them. The machine carries the rest: ~/Server/<Name>/ is a
|
||||
# real directory owning the .env (secrets never sit inside the checkout,
|
||||
# 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,
|
||||
# gitignored or not) and the bind-mounted data. Only the tracked files are
|
||||
# symlinked, so a `git clean` in the repo can never reach a database.
|
||||
#
|
||||
@@ -29,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"
|
||||
@@ -48,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
|
||||
@@ -85,12 +83,12 @@ require_service() {
|
||||
fi
|
||||
}
|
||||
|
||||
# The one compose unit a service directory carries. Its filename is its
|
||||
# identity -- podman-<name>.service, exactly what lands in the systemd user
|
||||
# directory -- so nothing here invents a name that could drift from the file's.
|
||||
# The one unit file a service directory carries. Its filename is its identity
|
||||
# -- podman-<name>.service, exactly what lands in the systemd user directory --
|
||||
# so nothing here invents a name that could drift from the file's.
|
||||
unit_path_for() {
|
||||
local name="$1" unit
|
||||
for unit in "$SERVER_DIR/$name"/podman-*.service; do
|
||||
for unit in "$SERVER_DIR/$name"/*.service; do
|
||||
[[ -e "$unit" ]] || continue
|
||||
printf '%s' "$unit"
|
||||
return 0
|
||||
@@ -98,44 +96,6 @@ unit_path_for() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# Every other unit in the directory: helper services and the timers that fire
|
||||
# them. They are linked beside the compose unit, and each timer is enabled.
|
||||
helper_units_for() {
|
||||
local name="$1" unit
|
||||
for unit in "$SERVER_DIR/$name"/*.service "$SERVER_DIR/$name"/*.timer; do
|
||||
[[ -e "$unit" ]] || continue
|
||||
[[ "$(basename "$unit")" == podman-*.service ]] && continue
|
||||
printf '%s\n' "$unit"
|
||||
done
|
||||
}
|
||||
|
||||
# The files linked into ~/Server/<Name>/: every visible top-level file that is
|
||||
# not a unit. That is compose.yml plus any helper script or README; dotfiles
|
||||
# are skipped so .env.example (and a stray .env) never become links.
|
||||
live_files_for() {
|
||||
local name="$1" file
|
||||
for file in "$SERVER_DIR/$name"/*; do
|
||||
[[ -f "$file" ]] || continue
|
||||
case "$file" in *.service|*.timer) continue ;; esac
|
||||
printf '%s\n' "$file"
|
||||
done
|
||||
}
|
||||
|
||||
link_service_files() {
|
||||
local name="$1" file unit
|
||||
mkdir -p "$TARGET_DIR/$name" "$UNIT_DIR"
|
||||
while IFS= read -r file; do
|
||||
link_file "$file" "$TARGET_DIR/$name/$(basename "$file")"
|
||||
done < <(live_files_for "$name")
|
||||
while IFS= read -r unit; do
|
||||
link_file "$unit" "$UNIT_DIR/$(basename "$unit")"
|
||||
done < <(helper_units_for "$name")
|
||||
}
|
||||
|
||||
helper_timers_for() {
|
||||
helper_units_for "$1" | grep '\.timer$' | xargs -r -n1 basename
|
||||
}
|
||||
|
||||
unit_name_for() {
|
||||
local unit
|
||||
unit="$(unit_path_for "$1")" || return 1
|
||||
@@ -151,12 +111,10 @@ unit_installed() {
|
||||
[[ -L "$UNIT_DIR/$unit_name" ]]
|
||||
}
|
||||
|
||||
# Everything this machine links for the service, so a change to a helper
|
||||
# script or timer is reported by relink just like a compose change.
|
||||
definition_hash() {
|
||||
local name="$1"
|
||||
{ live_files_for "$name"; unit_path_for "$name" && echo; helper_units_for "$name"; } \
|
||||
| sort | xargs -r -d '\n' cat 2>/dev/null | sha256sum | cut -d' ' -f1
|
||||
local name="$1" unit
|
||||
unit="$(unit_path_for "$name")" || unit=/dev/null
|
||||
cat "$SERVER_DIR/$name/compose.yml" "$unit" 2>/dev/null | sha256sum | cut -d' ' -f1
|
||||
}
|
||||
|
||||
recorded_hash() {
|
||||
@@ -240,7 +198,6 @@ cmd_enable() {
|
||||
fi
|
||||
|
||||
mkdir -p "$UNIT_DIR"
|
||||
link_service_files "$name"
|
||||
link_file "$unit_path" "$UNIT_DIR/$unit_name"
|
||||
systemctl --user daemon-reload
|
||||
if systemctl --user enable --now "$unit_name"; then
|
||||
@@ -250,10 +207,6 @@ cmd_enable() {
|
||||
err "$unit_name did not start; see: systemctl --user status $unit_name"
|
||||
exit 1
|
||||
fi
|
||||
local timer
|
||||
for timer in $(helper_timers_for "$name"); do
|
||||
systemctl --user enable --now "$timer" && ok "Enabled $timer"
|
||||
done
|
||||
}
|
||||
|
||||
cmd_disable() {
|
||||
@@ -263,15 +216,11 @@ cmd_disable() {
|
||||
local unit_name
|
||||
unit_name="$(unit_name_for "$name")" || { err "$name has no unit file in the catalog"; exit 1; }
|
||||
|
||||
local timer unit
|
||||
for timer in $(helper_timers_for "$name"); do
|
||||
systemctl --user disable --now "$timer" 2>/dev/null || true
|
||||
done
|
||||
systemctl --user disable --now "$unit_name" 2>/dev/null || true
|
||||
for unit in "$UNIT_DIR/$unit_name" $(helper_units_for "$name" | xargs -r -n1 basename | sed "s|^|$UNIT_DIR/|"); do
|
||||
[[ -L "$unit" ]] && rm "$unit"
|
||||
done
|
||||
systemctl --user daemon-reload
|
||||
if [[ -L "$UNIT_DIR/$unit_name" ]]; then
|
||||
rm "$UNIT_DIR/$unit_name"
|
||||
systemctl --user daemon-reload
|
||||
fi
|
||||
ok "$name disabled. ~/Server/$name, its .env and its data were not touched."
|
||||
}
|
||||
|
||||
@@ -318,7 +267,8 @@ cmd_relink() {
|
||||
unit_installed "$name" || continue
|
||||
|
||||
link_file "$unit_path" "$UNIT_DIR/$unit_name"
|
||||
link_service_files "$name"
|
||||
mkdir -p "$TARGET_DIR/$name"
|
||||
link_file "$SERVER_DIR/$name/compose.yml" "$TARGET_DIR/$name/compose.yml"
|
||||
refreshed=$((refreshed + 1))
|
||||
|
||||
# Changed since the last time this machine looked, which is what makes a
|
||||
|
||||
@@ -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 {}'"
|
||||
|
||||
+4
-20
@@ -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"
|
||||
@@ -47,9 +32,8 @@ export DOTNETPATH="$HOME/.dotnet/tools"
|
||||
# Podman
|
||||
# export DOCKER_HOST=unix:///run/user/1000/podman/podman.sock
|
||||
|
||||
# Set complete path. ~/.opencode/bin is OpenCode 2's own installer location
|
||||
# and sits ahead of Bun's bin, which can still hold an OpenCode 1 copy.
|
||||
export PATH="$HOME/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PANAMA_PATH/bin:$HOME/.opencode/bin:$BUN_INSTALL/bin:$CARGO_PATH/bin:$PNPM_HOME/bin:$PYENV_ROOT/bin:$HOME/.rbenv/bin:/usr/lib/ccache/bin/:$GOPATH/bin:$DOTNETPATH"
|
||||
# Set complete path
|
||||
export PATH="$HOME/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PANAMA_PATH/bin:$BUN_INSTALL/bin:$CARGO_PATH/bin:$PNPM_HOME/bin:$PYENV_ROOT/bin:$HOME/.rbenv/bin:/usr/lib/ccache/bin/:$GOPATH/bin:$DOTNETPATH"
|
||||
|
||||
# rustup writes this file, and initial-packages installs rustup rather than
|
||||
# running rustup-init -- so on a fresh machine it does not exist yet and an
|
||||
|
||||
@@ -29,9 +29,3 @@ require("autostart")
|
||||
|
||||
-- Per-machine overrides, loaded last so it wins. Ships effectively empty.
|
||||
require("overrides")
|
||||
|
||||
-- T3 Code SnapShots. T3's setup wrote this line and finds it again by its exact
|
||||
-- shape (a bare hl.bind to a global), so keep it here and unwrapped. Going
|
||||
-- through keybinds.lua's bind() would make T3 add a second copy. Change or
|
||||
-- remove it from T3 Code under Settings > SnapShots.
|
||||
hl.bind("CTRL + SHIFT + 2", hl.dsp.global("com.t3tools.T3Code:capture-window"))
|
||||
@@ -8,14 +8,6 @@
|
||||
local prefs = require("prefs")
|
||||
local actions = require("actions")
|
||||
|
||||
-- Empty means "let libinput pick per device". Hyprland 0.57 rejects "" for
|
||||
-- this option, so empty is expressed by not writing it at all: nil drops the
|
||||
-- key from the table below and the option stays at its unset default.
|
||||
local scroll_method = prefs.get("scrollMethod", "")
|
||||
if scroll_method == "" then
|
||||
scroll_method = nil
|
||||
end
|
||||
|
||||
hl.config({
|
||||
input = {
|
||||
kb_layout = prefs.get("keyboardLayout", "us"),
|
||||
@@ -60,12 +52,16 @@ hl.config({
|
||||
scroll_factor = prefs.get("scrollFactor", 1.0),
|
||||
left_handed = prefs.get("leftHanded", false),
|
||||
|
||||
-- Empty (omitted, see above) is what `hyprctl getoption` reports as
|
||||
-- "[[EMPTY]]": libinput picks per device -- two fingers on a touchpad,
|
||||
-- the wheel on a mouse. Settings offers 2fg / edge / on_button_down /
|
||||
-- Empty is a value, not an omission: it is what `hyprctl getoption`
|
||||
-- reports as "[[EMPTY]]" before anything writes the option, and it means
|
||||
-- "let libinput pick per device" -- two fingers on a touchpad, the wheel
|
||||
-- on a mouse. Writing it back explicitly is the same branch a stock
|
||||
-- Hyprland takes, and it keeps the setting reversible: without an empty
|
||||
-- choice in the schema there would be no way back from a scroll method
|
||||
-- once one was picked. Settings offers 2fg / edge / on_button_down /
|
||||
-- no_scroll alongside it, the four words the option's own description
|
||||
-- names (it publishes no map).
|
||||
scroll_method = scroll_method,
|
||||
scroll_method = prefs.get("scrollMethod", ""),
|
||||
|
||||
-- Only consulted while scroll_method is on_button_down. 0 means the
|
||||
-- device's own middle button.
|
||||
|
||||
@@ -377,18 +377,15 @@ bind(mod .. " + SHIFT + N", hl.dsp.window.resize({ x = 0, y = step, relative = t
|
||||
-- The gesture needs three binds, not two. Tab steps the selection, and the
|
||||
-- switch is only COMMITTED when the modifier is released -- which is the sole
|
||||
-- way the compositor can tell the gesture is finished. That release bind is on
|
||||
-- the Super keys themselves, so it fires on EVERY Super release in the
|
||||
-- session; the handler returns immediately when no switch is open, which is
|
||||
-- why this is affordable.
|
||||
-- the bare modifier, so it fires on EVERY Super release in the session; the
|
||||
-- handler returns immediately when no switch is open, which is why this is
|
||||
-- affordable.
|
||||
--
|
||||
-- A modifier-only bind has to name the keysym (Super_L / Super_R). Hyprland
|
||||
-- 0.57 rejects a chord that is a bare modifier ("A bind requires a trigger"),
|
||||
-- and the error stops this file, costing every bind below it.
|
||||
-- The release bind carries no description on purpose: it is not a shortcut
|
||||
-- anyone would look up or rebind, and the Shortcuts page lists what it finds.
|
||||
bind(mod .. " + Tab", hl.dsp.exec_cmd(qs("switcher", "next")), { description = "Next window" })
|
||||
bind(mod .. " + SHIFT + Tab", hl.dsp.exec_cmd(qs("switcher", "previous")), { description = "Previous window" })
|
||||
for _, key in ipairs({ "Super_L", "Super_R" }) do
|
||||
bind(mod .. " + " .. key, hl.dsp.exec_cmd(qs("switcher", "commit")), { release = true, description = "Commit window switch" })
|
||||
end
|
||||
bind(mod, hl.dsp.exec_cmd(qs("switcher", "commit")), { release = true, description = "Commit window switch" })
|
||||
-- Jump back to the previously focused window.
|
||||
bind(mod .. " + SHIFT + grave", hl.dsp.focus({ last = true }), { description = "Last window" })
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -184,8 +184,7 @@ Singleton {
|
||||
options: [
|
||||
{ value: "none", label: "None" },
|
||||
{ value: "claude", label: "Claude Code" },
|
||||
{ value: "codex", label: "Codex" },
|
||||
{ value: "t3", label: "T3 Code" }
|
||||
{ value: "codex", label: "Codex" }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -206,7 +205,7 @@ Singleton {
|
||||
{
|
||||
key: "agentAutoApprove", type: "bool", def: true, group: "agents",
|
||||
label: "Launched agents approve their own tools",
|
||||
detail: "Investigations run without permission prompts. T3 Code threads use the mode set on the Panama project in T3 instead. The diagnose skill still holds agents to reading rather than fixing, and root still goes through panama-sudo, reason and all"
|
||||
detail: "Investigations run without permission prompts. The diagnose skill still holds agents to reading rather than fixing, and root still goes through panama-sudo, reason and all"
|
||||
},
|
||||
{
|
||||
key: "agentUsageClaude", type: "bool", def: true, group: "agents",
|
||||
@@ -1806,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 {
|
||||
|
||||
@@ -37,8 +37,8 @@ SettingsPage {
|
||||
// them not to believe the page.
|
||||
//
|
||||
// Probed through a LOGIN shell rather than this one. Quickshell is started
|
||||
// by systemd, whose PATH does not include ~/.local/bin -- where all three of
|
||||
// these usually land (T3 Code's CLI is `t3`) -- and a login shell is the environment the launcher
|
||||
// by systemd, whose PATH does not include ~/.local/bin -- where both of
|
||||
// these usually land -- and a login shell is the environment the launcher
|
||||
// hands the agent when it spawns a terminal. Asking with the shell's own
|
||||
// PATH would report "not installed" for an agent that starts perfectly.
|
||||
property var installed: ({})
|
||||
@@ -60,7 +60,7 @@ SettingsPage {
|
||||
|
||||
running: true
|
||||
command: ["bash", "-lc",
|
||||
"for agent in claude codex t3; do command -v \"$agent\" >/dev/null 2>&1 && printf '%s\\n' \"$agent\"; done"]
|
||||
"for agent in claude codex; do command -v \"$agent\" >/dev/null 2>&1 && printf '%s\\n' \"$agent\"; done"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: root.absorbProbe(this.text)
|
||||
}
|
||||
@@ -70,7 +70,6 @@ SettingsPage {
|
||||
switch (value) {
|
||||
case "claude": return "starred-symbolic";
|
||||
case "codex": return "utilities-terminal-symbolic";
|
||||
case "t3": return "view-dual-symbolic";
|
||||
default: return "notifications-disabled-symbolic";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -487,11 +487,11 @@ Singleton {
|
||||
if ((bind.modmask & modifier.bit) !== 0)
|
||||
parts.push(modifier.name.toUpperCase());
|
||||
}
|
||||
// Guard against a bind with no key. Hyprland 0.57 refuses to create
|
||||
// one (the switcher's Super-release commit now names Super_L/Super_R),
|
||||
// but appending an empty string would leave a dangling "SUPER + " that
|
||||
// matches neither the chord hypr/keybinds.lua binds nor the one an
|
||||
// override would be keyed by.
|
||||
// A modifier-only bind has no key at all -- the window switcher commits
|
||||
// on Super RELEASE. Appending an empty string left a dangling "SUPER + "
|
||||
// that matched neither the chord hypr/keybinds.lua binds nor the one an
|
||||
// override would be keyed by, so that bind could never be rebound and
|
||||
// never found its category.
|
||||
const key = String(bind.key ?? "");
|
||||
if (key !== "")
|
||||
parts.push(key);
|
||||
|
||||
@@ -356,7 +356,6 @@ Singleton {
|
||||
{ label: "AI assistant", detail: "Choose the agent this desktop hands crashes, failed reloads and red health checks to", page: "agents" },
|
||||
{ label: "Claude Code", detail: "Use Claude Code as the agent the desktop escalates to, and watch its usage", page: "agents" },
|
||||
{ label: "Codex", detail: "Use Codex as the agent the desktop escalates to, and watch its usage", page: "agents" },
|
||||
{ label: "T3 Code", detail: "Open a T3 Code thread in the Panama project when the desktop escalates something", page: "agents" },
|
||||
{ label: "Diagnose crashes", detail: "When a program dumps core, the notification carries a click that opens your AI assistant with the crash details", page: "agents" },
|
||||
{ label: "Ask the agent", detail: "A red System Health check with no repair left hands its snapshot to your AI assistant", page: "agents" },
|
||||
{ label: "Agent usage panel", detail: "Limits, resets and tokens for each agent, opened from the bar", page: "agents" },
|
||||
|
||||
@@ -5,6 +5,6 @@
|
||||
# @vicinae.mode silent
|
||||
# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg
|
||||
# @vicinae.description Open Agents in Settings.
|
||||
# @vicinae.keywords ["settings", "preferred agent", "offer to diagnose crashes", "offer help when the shell fails to reload", "system health hands off unrepairable checks", "launched agents approve their own tools", "collect claude code usage", "collect codex usage", "refresh interval", "ai assistant", "claude code", "codex", "t3 code"]
|
||||
# @vicinae.keywords ["settings", "preferred agent", "offer to diagnose crashes", "offer help when the shell fails to reload", "system health hands off unrepairable checks", "launched agents approve their own tools", "collect claude code usage", "collect codex usage", "refresh interval", "ai assistant", "claude code", "codex", "diagnose crashes"]
|
||||
|
||||
exec "$HOME/.config/quickshell/scripts/panama-action" settings-page agents
|
||||
@@ -1,55 +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"
|
||||
# ~/.opencode/bin is OpenCode 2's own installer location, ahead of Bun's bin.
|
||||
export PATH="$HOME/.local/bin:$HOME/.opencode/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")"
|
||||
+2
-2
@@ -28,11 +28,11 @@ Found on **System › Agents**.
|
||||
|
||||
| Setting | Default | What it does |
|
||||
|---|---|---|
|
||||
| **Preferred agent**<br>`preferredAgent` | none | Who answers when the desktop offers to investigate something Choices: None, Claude Code, Codex, T3 Code. |
|
||||
| **Preferred agent**<br>`preferredAgent` | none | Who answers when the desktop offers to investigate something Choices: None, Claude Code, Codex. |
|
||||
| **Offer to diagnose crashes**<br>`crashDiagnoseOffer` | true | When a program dumps core, the notification carries a click that opens the preferred agent mid-investigation with the crash details in hand |
|
||||
| **Offer help when the shell fails to reload**<br>`reloadFailureOffer` | true | A broken change to the shell's own configuration offers the failing log to the agent |
|
||||
| **System Health hands off unrepairable checks**<br>`healthAgentHandoff` | true | A red check with no repair, or whose repair failed, grows an Ask-the-agent button carrying the check's snapshot |
|
||||
| **Launched agents approve their own tools**<br>`agentAutoApprove` | true | Investigations run without permission prompts. T3 Code threads use the mode set on the Panama project in T3 instead. The diagnose skill still holds agents to reading rather than fixing, and root still goes through panama-sudo, reason and all |
|
||||
| **Launched agents approve their own tools**<br>`agentAutoApprove` | true | Investigations run without permission prompts. The diagnose skill still holds agents to reading rather than fixing, and root still goes through panama-sudo, reason and all |
|
||||
| **Collect Claude Code usage**<br>`agentUsageClaude` | true | Limits from Anthropic's usage endpoint, tokens from the local transcripts |
|
||||
| **Collect Codex usage**<br>`agentUsageCodex` | true | Limits over the Codex app-server, sessions from its local files |
|
||||
| **Refresh interval**<br>`agentUsageRefreshMinutes` | 15 min | How often the usage collectors ask for fresh numbers, in minutes. Range 5–60. |
|
||||
|
||||
@@ -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."
|
||||
+7
-74
@@ -6,44 +6,18 @@ 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.
|
||||
Loose files beside the directories are the VPS's own
|
||||
host tooling (see "Host tooling" below)
|
||||
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`).
|
||||
`Media/compact-tv/` is the nightly TV re-encode job that runs beside the Media
|
||||
stack, with its own README.
|
||||
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.
|
||||
`panama server enable <Name>` creates `~/Server/<Name>/` as a **real
|
||||
directory** and symlinks the service's visible top-level files into it:
|
||||
`compose.yml`, plus any helper script or README. The units symlink into
|
||||
directory** and symlinks only `compose.yml` into it; the unit symlinks into
|
||||
`~/.config/systemd/user/`. The `.env` (seeded once from `.env.example`, then
|
||||
yours) and the bind-mounted `data/` live in `~/Server/<Name>/` and are never
|
||||
inside the checkout — so nothing git can do, `clean -fdx` included, can reach
|
||||
@@ -61,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.
|
||||
@@ -99,46 +67,11 @@ 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.
|
||||
|
||||
A service that needs a scheduled job of its own carries it beside the
|
||||
compose unit: a helper `<job>.service` and a `<job>.timer` with the same
|
||||
name, plus the script it runs. Enable links all three and enables the timer.
|
||||
Disable stops the timer and unlinks the units. `UseSend` is the example: its
|
||||
`usesend-cert-refresh` timer keeps the SMTP proxy's certificate in step with
|
||||
Nginx Proxy Manager's.
|
||||
|
||||
## Host tooling
|
||||
|
||||
Each server's own scripts and runbooks sit as loose files at the top of its
|
||||
tree. They are reference copies for rebuilding a machine. Nothing installs
|
||||
them.
|
||||
|
||||
- `vps/backup`, `vps/server-backup.service`, `vps/server-backup.timer`: the
|
||||
VPS's nightly backup, run as a user unit. It dumps every database into
|
||||
`~/Server/_backup_state/`, then rsyncs `~/Server` to the home server's ZFS
|
||||
pool with `--fake-super`, so subuid ownership survives the trip.
|
||||
- `vps/verify-rootless`: the post-reboot health check for the rootless
|
||||
stack. It covers linger, the unprivileged port floor, unit state and boot
|
||||
enablement, postgres, endpoints, and admin ports bound to the WireGuard
|
||||
address.
|
||||
- `vps/AGENTS.md`: the VPS runbook, with both public IPs redacted.
|
||||
- `home/RESTORE.md`: how to restore the home server's `~/Server` from the
|
||||
ZFS backup.
|
||||
- `home/backup` and `home/systemd/`: the home server's backup trigger and its
|
||||
root system units (`server-backup` nightly, `media-backup` weekly).
|
||||
- `home/server-backup` and `home/media-backup`: the scripts those units run.
|
||||
On that machine they live in `/usr/local/sbin`, root-owned, because a
|
||||
root-run script must not sit in a directory the user can write to. Restore
|
||||
them there, not into `~/Server`.
|
||||
|
||||
The VPS units carry `RequiresMountsFor=/home/gib/Media` because that host
|
||||
keeps its podman graphroot on an attached volume mounted there. The setting
|
||||
is harmless on a machine without the mount.
|
||||
|
||||
## Nightly updates
|
||||
|
||||
`server/scripts/update-containers` pulls images and restarts changed
|
||||
|
||||
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.
File renamed without changes.
@@ -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.
|
||||
File renamed without changes.
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
|
||||
+2
-2
@@ -29,7 +29,7 @@ services:
|
||||
restart: unless-stopped
|
||||
|
||||
convexmonorepo-backend:
|
||||
image: ghcr.io/convex/convex-backend:${BACKEND_TAG:-latest}
|
||||
image: ghcr.io/get-convex/convex-backend:${BACKEND_TAG:-latest}
|
||||
container_name: ${BACKEND_CONTAINER_NAME:-convex-backend}
|
||||
hostname: ${BACKEND_CONTAINER_NAME:-convex-backend}
|
||||
domainname: ${BACKEND_DOMAIN:-convex.gbrown.org}
|
||||
@@ -58,7 +58,7 @@ services:
|
||||
stop_signal: SIGINT
|
||||
|
||||
convexmonorepo-dashboard:
|
||||
image: ghcr.io/convex/convex-dashboard:${DASHBOARD_TAG:-latest}
|
||||
image: ghcr.io/get-convex/convex-dashboard:${DASHBOARD_TAG:-latest}
|
||||
container_name: ${DASHBOARD_CONTAINER_NAME:-convex-dashboard}
|
||||
hostname: ${DASHBOARD_CONTAINER_NAME:-convex-dashboard}
|
||||
domainname: ${DASHBOARD_DOMAIN:-dashboard.${BACKEND_DOMAIN:-convex.gbrown.org}}
|
||||
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.
File renamed without changes.
@@ -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.
File renamed without changes.
@@ -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}
|
||||
@@ -19,7 +13,7 @@ services:
|
||||
- NODE_ENV=${NODE_ENV}
|
||||
- SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN}
|
||||
- NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL:-http://localhost:${NEXT_PORT:-3000}}
|
||||
- NEXT_PUBLIC_CONVEX_URL=${CONVEX_SERVER_URL:-http://${BACKEND_CONTAINER_NAME:-lashaddict-backend}:${BACKEND_PORT:-3210}}
|
||||
- NEXT_PUBLIC_CONVEX_URL=${NEXT_PUBLIC_CONVEX_URL:-http://${BACKEND_CONTAINER_NAME:-lashaddict-backend}:${BACKEND_PORT:-3210}}
|
||||
- NEXT_PUBLIC_PLAUSIBLE_URL=${NEXT_PUBLIC_PLAUSIBLE_URL:-https://plausible.gbrown.org}
|
||||
- NEXT_PUBLIC_SENTRY_DSN=${NEXT_PUBLIC_SENTRY_DSN}
|
||||
- NEXT_PUBLIC_SENTRY_URL=${NEXT_PUBLIC_SENTRY_URL}
|
||||
@@ -33,13 +27,8 @@ services:
|
||||
stdin_open: true
|
||||
restart: unless-stopped
|
||||
lashaddict-backend:
|
||||
image: ghcr.io/convex/convex-backend:${BACKEND_TAG:-latest}
|
||||
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:
|
||||
@@ -67,13 +56,8 @@ services:
|
||||
stop_grace_period: 10s
|
||||
stop_signal: SIGINT
|
||||
lashaddict-dashboard:
|
||||
image: ghcr.io/convex/convex-dashboard:${DASHBOARD_TAG:-latest}
|
||||
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.
File renamed without changes.
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'
|
||||
File renamed without changes.
@@ -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.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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
|
||||
File renamed without changes.
@@ -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
|
||||
@@ -64,7 +64,7 @@ services:
|
||||
security_opt:
|
||||
- label:disable
|
||||
spoon-backend:
|
||||
image: ghcr.io/convex/convex-backend:${BACKEND_TAG:-latest}
|
||||
image: ghcr.io/get-convex/convex-backend:${BACKEND_TAG:-latest}
|
||||
container_name: ${BACKEND_CONTAINER_NAME:-spoon-backend}
|
||||
hostname: ${BACKEND_CONTAINER_NAME:-spoon-backend}
|
||||
domainname: ${BACKEND_DOMAIN:-convex.spoon.gbrown.org}
|
||||
@@ -93,7 +93,7 @@ services:
|
||||
stop_grace_period: 10s
|
||||
stop_signal: SIGINT
|
||||
spoon-dashboard:
|
||||
image: ghcr.io/convex/convex-dashboard:${DASHBOARD_TAG:-latest}
|
||||
image: ghcr.io/get-convex/convex-dashboard:${DASHBOARD_TAG:-latest}
|
||||
container_name: ${DASHBOARD_CONTAINER_NAME:-spoon-dashboard}
|
||||
hostname: ${DASHBOARD_CONTAINER_NAME:-spoon-dashboard}
|
||||
domainname: ${DASHBOARD_DOMAIN:-dashboard.${BACKEND_DOMAIN:-spoon.gbrown.org}}
|
||||
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
|
||||
@@ -29,7 +29,7 @@ services:
|
||||
restart: unless-stopped
|
||||
|
||||
stpeteit-backend:
|
||||
image: ghcr.io/convex/convex-backend:${BACKEND_TAG:-latest}
|
||||
image: ghcr.io/get-convex/convex-backend:${BACKEND_TAG:-latest}
|
||||
container_name: ${BACKEND_CONTAINER_NAME:-stpeteit-backend}
|
||||
hostname: ${BACKEND_CONTAINER_NAME:-stpeteit-backend}
|
||||
domainname: ${BACKEND_DOMAIN:-convex.stpeteit.com}
|
||||
@@ -58,7 +58,7 @@ services:
|
||||
stop_signal: SIGINT
|
||||
|
||||
stpeteit-dashboard:
|
||||
image: ghcr.io/convex/convex-dashboard:${DASHBOARD_TAG:-latest}
|
||||
image: ghcr.io/get-convex/convex-dashboard:${DASHBOARD_TAG:-latest}
|
||||
container_name: ${DASHBOARD_CONTAINER_NAME:-stpeteit-dashboard}
|
||||
hostname: ${DASHBOARD_CONTAINER_NAME:-stpeteit-dashboard}
|
||||
domainname: ${DASHBOARD_DOMAIN:-dashboard.${BACKEND_DOMAIN:-convex.stpete.com}}
|
||||
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
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -8,22 +8,15 @@ AWS_DEFAULT_REGION=us-east-2
|
||||
DATABASE_URL=CHANGE_ME
|
||||
GITHUB_ID=Ov23liz34colyJed9t9S
|
||||
GITHUB_SECRET=CHANGE_ME
|
||||
MINIO_ROOT_USER=usesend
|
||||
MINIO_ROOT_PASSWORD=CHANGE_ME
|
||||
NEXTAUTH_SECRET=CHANGE_ME
|
||||
NEXT_PUBLIC_IS_CLOUD=false
|
||||
POSTGRES_USER=usesend
|
||||
POSTGRES_PASSWORD=CHANGE_ME
|
||||
POSTGRES_DB=usesend
|
||||
REDIS_URL=redis://usesend-redis:6379
|
||||
RUSTFS_ACCESS_KEY=CHANGE_ME
|
||||
RUSTFS_SECRET_KEY=CHANGE_ME
|
||||
S3_COMPATIBLE_API_URL=https://storage.usesend.gbrown.org
|
||||
S3_COMPATIBLE_PUBLIC_URL=https://storage.usesend.gbrown.org/unsend
|
||||
S3_COMPATIBLE_BUCKET=unsend
|
||||
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
|
||||
File renamed without changes.
@@ -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
|
||||
Loaded 100 of 274 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user