Compare commits

...

26 Commits

Author SHA1 Message Date
yukkop 557b6e9ef0 fix: migrator 2026-07-24 15:21:19 +04:00
yukkop e49f497045 fix: aga 2026-07-24 15:21:11 +04:00
yukkop feb1a48db1 feat: some 2026-07-23 14:19:58 +04:00
yukkop 30732080b7 feat: some 2026-07-21 16:57:10 +04:00
yukkop 80cf1588bb feat: +darwin config for yukkop
runner nix smoke / nix label smoke (push) Has been cancelled
2026-07-19 20:03:18 +04:00
yukkop ef7d1b29f4 feat: +njalla module 2026-07-13 23:37:14 +00:00
yukkop 7e8c6884db feat: +plgo 2026-07-13 13:27:04 +00:00
yukkop 1dd41e608b feat: neuro: stable video diffusion 2026-07-05 16:58:47 +00:00
yukkop e41c3e5a05 ssh fixes 2026-07-04 19:32:08 +00:00
yukkop f473280bf5 fix: matrix hardcode 2026-07-01 09:44:43 +00:00
yukkop b08fdd6e6b fix: matrix media 2026-07-01 09:23:26 +00:00
yukkop f73bfc63be fix: zomro: reboot 2026-06-20 22:00:52 +00:00
yukkop f6e7c1eca9 chore 2026-06-11 14:39:57 +00:00
yukkop 6996d178ef fix: hetzner: newer servers generation 2026-06-10 15:39:05 +00:00
yukkop 7f7229b199 feat: tenix host
runner nix smoke / nix label smoke (push) Has been cancelled
2026-06-10 13:15:09 +00:00
yukkop 2d5bd26c36 docs: migrator: ~ logs & comments 2026-06-10 12:26:16 +00:00
yukkop fba150b55b docs: db-tool: ~ postgres-init and postgres-cleanup 2026-06-10 12:14:23 +00:00
yukkop 2e7bf58acf refactor: db-tool: rename file 2026-06-10 11:59:21 +00:00
yukkop b12c35f957 fix: db-tool 2026-06-10 11:40:35 +00:00
yukkop bcf1b84dc4 fix: db-tool 2026-06-10 11:32:37 +00:00
yukkop 7c25e3b46d feat: db-tool: +secrets load 2026-06-09 23:57:51 +00:00
yukkop a20381e343 chore: ssh key 2026-06-09 22:35:06 +00:00
yukkop bd92610a98 feat: floating ip 2026-06-09 15:43:23 +00:00
yukkop e3ee881db6 chore: lab: neuro's ssh key 2026-06-09 15:11:41 +00:00
yukkop b9eabca464 feat: load-sops: +generic 2026-06-09 14:51:11 +00:00
yukkop fcc72192f5 dev: gitea: runners devshell 2026-06-08 10:22:34 +00:00
55 changed files with 4845 additions and 368 deletions
+21
View File
@@ -0,0 +1,21 @@
{
flake,
self,
inputs,
system ? "aarch64-darwin",
...
}: let
name = builtins.baseNameOf ./.;
in inputs.nix-darwin.lib.darwinSystem {
inherit system;
specialArgs = { inherit flake self inputs; };
modules = [
inputs.home-manager.darwinModules.home-manager
{
networking.hostName = name;
nixpkgs.hostPlatform = system;
nixpkgs.overlays = [ self.overlays.default ];
}
./${name}.nix
];
}
+177
View File
@@ -0,0 +1,177 @@
{
flake,
pkgs,
lib,
...
}: let
name = "yukkop";
in {
system.primaryUser = name;
nix.settings.experimental-features = "nix-command flakes";
programs.zsh.enable = true;
services.openssh.enable = true;
users.users.${name} = {
home = "/Users/${name}";
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJf9ljuqny71bZJokebK4Ybfml0MFMCkApS+tbMdBudp u0_a472@localhost"
];
};
environment.systemPackages = with pkgs; [
aerospace
git
moreutils
neovim
tmux
];
launchd.user.agents.aerospace = {
serviceConfig = {
ProgramArguments = [
"${pkgs.aerospace}/Applications/AeroSpace.app/Contents/MacOS/AeroSpace"
];
RunAtLoad = true;
KeepAlive = true;
StandardOutPath = "/tmp/aerospace.out.log";
StandardErrorPath = "/tmp/aerospace.err.log";
};
};
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.backupFileExtension = "backup";
home-manager.sharedModules = [
(flake + "/home/module/program/tmux.nix")
];
home-manager.users.${name} = {
home.stateVersion = "25.11";
home.packages = with pkgs; [
iproute2mac
jujutsu
ripgrep
];
programs.git = {
enable = true;
lfs.enable = true;
settings = {
user.name = name;
user.email = "hectic.yukkop@gmail.com";
push.autoSetupRemote = true;
init.defaultBranch = "master";
};
};
programs.zsh = {
enable = true;
enableCompletion = true;
autosuggestion.enable = true;
syntaxHighlighting.enable = true;
history = {
size = 10000;
path = "$HOME/.zsh/.zsh_history";
};
shellAliases = {
drs = "darwin-rebuild switch --flake ~/pj/hearth#'yukkop|aarch64-darwin'";
nv = "nvim";
tmux = "tmux a";
};
initContent = ''
export PATH=/Users/yukkop/.opencode/bin:$PATH
'';
};
xdg.configFile."aerospace/aerospace.toml".text = ''
start-at-login = false
enable-normalization-flatten-containers = true
enable-normalization-opposite-orientation-for-nested-containers = true
default-root-container-layout = 'tiles'
default-root-container-orientation = 'auto'
accordion-padding = 30
on-focused-monitor-changed = ['move-mouse monitor-lazy-center']
automatically-unhide-macos-hidden-apps = false
[exec]
inherit-env-vars = true
[exec.env-vars]
PATH = '/run/current-system/sw/bin:/etc/profiles/per-user/yukkop/bin:/nix/var/nix/profiles/default/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:''${PATH}'
[gaps]
inner.horizontal = 8
inner.vertical = 8
outer.left = 8
outer.bottom = 8
outer.top = 8
outer.right = 8
[mode.main.binding]
alt-enter = 'exec-and-forget open -n /System/Applications/Utilities/Terminal.app'
alt-slash = 'layout tiles horizontal vertical'
alt-comma = 'layout accordion horizontal vertical'
alt-f = 'fullscreen'
alt-h = 'focus left'
alt-j = 'focus down'
alt-k = 'focus up'
alt-l = 'focus right'
alt-shift-h = 'move left'
alt-shift-j = 'move down'
alt-shift-k = 'move up'
alt-shift-l = 'move right'
alt-minus = 'resize smart -50'
alt-equal = 'resize smart +50'
alt-1 = 'workspace 1'
alt-2 = 'workspace 2'
alt-3 = 'workspace 3'
alt-4 = 'workspace 4'
alt-5 = 'workspace 5'
alt-6 = 'workspace 6'
alt-7 = 'workspace 7'
alt-8 = 'workspace 8'
alt-9 = 'workspace 9'
alt-shift-1 = 'move-node-to-workspace 1'
alt-shift-2 = 'move-node-to-workspace 2'
alt-shift-3 = 'move-node-to-workspace 3'
alt-shift-4 = 'move-node-to-workspace 4'
alt-shift-5 = 'move-node-to-workspace 5'
alt-shift-6 = 'move-node-to-workspace 6'
alt-shift-7 = 'move-node-to-workspace 7'
alt-shift-8 = 'move-node-to-workspace 8'
alt-shift-9 = 'move-node-to-workspace 9'
alt-tab = 'workspace-back-and-forth'
alt-shift-tab = 'move-workspace-to-monitor --wrap-around next'
alt-shift-semicolon = 'mode service'
[mode.service.binding]
esc = ['reload-config', 'mode main']
r = ['flatten-workspace-tree', 'mode main']
f = ['layout floating tiling', 'mode main']
b = ['balance-sizes', 'mode main']
backspace = ['close-all-windows-but-current', 'mode main']
alt-shift-h = ['join-with left', 'mode main']
alt-shift-j = ['join-with down', 'mode main']
alt-shift-k = ['join-with up', 'mode main']
alt-shift-l = ['join-with right', 'mode main']
'';
};
system.stateVersion = 6;
}
+1
View File
@@ -8,6 +8,7 @@
haskell = import ./haskell.nix { inherit self system pkgs; };
neuro = import ./neuro.nix { inherit self system pkgs; };
xmpp = import ./xmpp.nix { inherit self system pkgs; };
gitea-runners = import ./gitea-runners.nix { inherit pkgs; };
default = pkgs.mkShell {
buildInputs =
(with self.packages.${system}; [
+122
View File
@@ -0,0 +1,122 @@
{ pkgs, ... }: let
opentofuUnstable = "github:NixOS/nixpkgs/nixos-unstable#opentofu";
tofu = pkgs.writeShellScriptBin "tofu" ''
exec ${pkgs.nix}/bin/nix run ${opentofuUnstable} -- "$@"
'';
giteaRunnersSetup = pkgs.writeShellScriptBin "gitea-runners-setup" /* sh */ ''
cat <<'EOF'
Gitea runners setup checklist
Tools available in this shell:
tofu, kubectl, kustomize, kubeconform, sops, age, awscli2, hcloud, tea,
docker, skopeo, go-containerregistry, jq, yq-go, curl, git, openssh, nix
Environment expected before real deploy/apply:
TF_VAR_hcloud_token
TF_VAR_ssh_public_key
TF_VAR_ssh_private_key
S3 backend credentials and endpoint access
a matching SOPS age identity for sus/gitea-runners.yaml
kubectl access to the target cluster
a concrete registry digest for the pushed Nix-capable runner image if enabling
the nix label
OpenTofu validation gate:
tofu version
tofu -chdir=infra/gitea-runners/opentofu validate
Nix image build/publish/digest gate:
nix build .#gitea-runner-nix-image
publish the archive, then pin the registry-reported digest in the runner label
mapping
nix:docker://gitea.hectic-lab.com/hectic-lab/gitea-runner-nix-image@sha256:<registry-digest>
SOPS token Secret creation gate:
kubectl apply -f infra/gitea-runners/k8s/namespace.yaml
umask 077
token_file=$(mktemp /tmp/gitea-runner-token.XXXXXX)
trap 'rm -f "$token_file"' EXIT
sops -d --extract '["gitea"]["hectic-lab"]["org-runner-registration-token"]' sus/gitea-runners.yaml > "$token_file"
kubectl -n gitea-runners create secret generic gitea-runner-token \
--from-file=token="$token_file" \
--dry-run=client \
-o yaml | kubectl -n gitea-runners apply -f -
Cluster provision gate:
tofu -chdir=infra/gitea-runners/opentofu init
tofu -chdir=infra/gitea-runners/opentofu validate
tofu -chdir=infra/gitea-runners/opentofu plan -out=.sisyphus/evidence/task-12-deploy.plan
tofu -chdir=infra/gitea-runners/opentofu apply .sisyphus/evidence/task-12-deploy.plan
export KUBECONFIG="$(tofu -chdir=infra/gitea-runners/opentofu output -raw kubeconfig_path)"
Kubernetes apply gate:
kubectl config current-context
kubectl get nodes -o wide
kubectl get sc
kubectl apply -k infra/gitea-runners/k8s
Verification commands:
kubectl -n gitea-runners get statefulset gitea-runner
kubectl -n gitea-runners rollout status statefulset/gitea-runner --timeout=10m
kubectl -n gitea-runners get pods -l app.kubernetes.io/name=gitea-runner -o wide
kubectl -n gitea-runners get pvc -l app.kubernetes.io/name=gitea-runner -o wide
kubectl -n gitea-runners get events --sort-by=.lastTimestamp | tail -n 50
kubectl -n gitea-runners logs statefulset/gitea-runner -c runner --tail=200
Main blockers and gates:
do not run tofu apply without all external inputs
do not apply the k8s overlay until the gitea-runner-token Secret exists
do not enable the nix label until the image has been published with a concrete digest
do not print, load, or require secrets on shell entry
EOF
'';
in pkgs.mkShell {
name = "gitea-runners";
buildInputs = [
tofu
giteaRunnersSetup
pkgs.nix
pkgs.kubectl
pkgs.kustomize
pkgs.kubeconform
pkgs.sops
pkgs.age
pkgs.awscli2
pkgs.hcloud
pkgs.tea
pkgs.docker
pkgs.skopeo
pkgs.go-containerregistry
pkgs.jq
pkgs.yq-go
pkgs.curl
pkgs.git
pkgs.openssh
];
shellHook = ''
export GITEA_RUNNERS_ROOT="$PWD/infra/gitea-runners"
export GITEA_RUNNERS_TOFU_DIR="$GITEA_RUNNERS_ROOT/opentofu"
export GITEA_RUNNERS_K8S_DIR="$GITEA_RUNNERS_ROOT/k8s"
export GITEA_RUNNERS_IMAGE_DIR="$GITEA_RUNNERS_ROOT/image"
export GITEA_RUNNERS_NAMESPACE="gitea-runners"
alias cd-gitea-runners='cd "$GITEA_RUNNERS_ROOT"'
alias cd-gitea-runners-tofu='cd "$GITEA_RUNNERS_TOFU_DIR"'
alias cd-gitea-runners-k8s='cd "$GITEA_RUNNERS_K8S_DIR"'
echo ""
echo "=== Gitea runner setup DevShell ==="
echo ""
echo "Run gitea-runners-setup for the full setup checklist."
echo "Paths: "
echo " root=$GITEA_RUNNERS_ROOT"
echo " tofu=$GITEA_RUNNERS_TOFU_DIR"
echo " k8s=$GITEA_RUNNERS_K8S_DIR"
echo " image=$GITEA_RUNNERS_IMAGE_DIR"
echo ""
'';
}
Generated
+22
View File
@@ -688,6 +688,27 @@
"url": "ssh://git@github.com/LysmiMx/mechabellum-replay-analysis.git"
}
},
"nix-darwin": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1772129556,
"narHash": "sha256-Utk0zd8STPsUJPyjabhzPc5BpPodLTXrwkpXBHYnpeg=",
"owner": "nix-darwin",
"repo": "nix-darwin",
"rev": "ebec37af18215214173c98cf6356d0aca24a2585",
"type": "github"
},
"original": {
"owner": "nix-darwin",
"ref": "nix-darwin-25.11",
"repo": "nix-darwin",
"type": "github"
}
},
"nix-minecraft": {
"inputs": {
"flake-compat": "flake-compat_3",
@@ -967,6 +988,7 @@
"hyprland": "hyprland",
"impermanence": "impermanence",
"mechabellum-replay-analysis": "mechabellum-replay-analysis",
"nix-darwin": "nix-darwin",
"nix-minecraft": "nix-minecraft",
"nixos-anywhere": "nixos-anywhere",
"nixos-hardware": "nixos-hardware",
+8
View File
@@ -42,6 +42,10 @@
url = "github:nix-community/home-manager/release-25.11";
inputs.nixpkgs.follows = "nixpkgs";
};
nix-darwin = {
url = "github:nix-darwin/nix-darwin/nix-darwin-25.11";
inputs.nixpkgs.follows = "nixpkgs";
};
nixos-wsl = {
url = "github:nix-community/NixOS-WSL";
inputs.nixpkgs.follows = "nixpkgs";
@@ -120,7 +124,11 @@
"neuro|x86_64-linux" = import ./nixos/system/neuro { inherit flake self inputs; system = "x86_64-linux"; };
"games|x86_64-linux" = import ./nixos/system/games { inherit flake self inputs; system = "x86_64-linux"; };
"wsl|x86_64-linux" = import ./nixos/system/wsl { inherit flake self inputs; system = "x86_64-linux"; };
"tenix|x86_64-linux" = import ./nixos/system/tenix { inherit flake self inputs; system = "x86_64-linux"; };
"hectic-lab|x86_64-linux" = import ./nixos/system/hectic-lab { inherit flake self inputs; system = "x86_64-linux"; };
};
darwinConfigurations = {
"yukkop|aarch64-darwin" = import ./darwin/system/yukkop { inherit flake self inputs; system = "aarch64-darwin"; };
};
};
}
+30
View File
@@ -0,0 +1,30 @@
{ pkgs, ... }: {
programs.tmux = {
enable = true;
plugins = with pkgs.tmuxPlugins; [ resurrect continuum ];
keyMode = "vi";
escapeTime = 500;
historyLimit = 50000;
newSession = true;
extraConfig = ''
# resurrect
set -g @resurrect-strategy-vim 'session'
set -g @resurrect-strategy-nvim 'session'
set -g @resurrect-capture-pane-contents 'on'
resurrect_dir="$HOME/.tmux/resurrect"
set -g @resurrect-dir $resurrect_dir
set -g @resurrect-hook-post-save-all 'target=$(readlink -f $resurrect_dir/last); sed "s| --cmd .*-vim-pack-dir||g; s|/etc/profiles/per-user/$USER/bin/||g; s|/home/$USER/.nix-profile/bin/||g" $target | sponge $target'
# continuum
set -g @continuum-restore 'on'
set -g @continuum-boot 'on'
set -g @continuum-save-interval '10'
bind-key -T copy-mode-vi v send-keys -X begin-selection
bind-key -T copy-mode-vi C-v send-keys -X rectangle-toggle
bind-key O select-pane -t :.-
'';
};
}
+30 -2
View File
@@ -26,12 +26,39 @@
"aarch64-darwin"
];
cudaUnfreeNames = [
"cuda_nvcc"
"cuda_cudart"
"cuda_cuobjdump"
"cuda_cupti"
"cuda_nvdisasm"
"cuda_cccl"
"cuda_nvml_dev"
"cuda_nvrtc"
"cuda_nvtx"
"cuda_profiler_api"
"libcusparse_lt"
"libcublas"
"libcufft"
"libcufile"
"libcurand"
"libcusolver"
"libnvjitlink"
"libcusparse"
"cudnn"
];
cudaUnfreePredicate = pkg:
builtins.elem (nixpkgs.lib.getName pkg) cudaUnfreeNames;
forSystemsWithPkgs = supportedSystems: pkgOverlays: f:
builtins.foldl' (
acc: system: let
pkgs = import nixpkgs {
inherit system;
overlays = pkgOverlays;
config.allowUnfreePredicate = cudaUnfreePredicate;
};
systemOutputs = f {
system = system;
@@ -58,7 +85,7 @@
else {};
in {
# -- For all systems --
inherit dotEnv minorEnvironment parseEnv forAllSystemsWithPkgs forSystemsWithPkgs commonSystems AllSystems;
inherit dotEnv minorEnvironment parseEnv forAllSystemsWithPkgs forSystemsWithPkgs commonSystems AllSystems cudaUnfreeNames cudaUnfreePredicate;
forSystems = systems: nixpkgs.lib.genAttrs systems;
forAllSystems = nixpkgs.lib.genAttrs AllSystems;
@@ -67,6 +94,7 @@ in {
logs = builtins.readFile ./shell/logs.sh;
check-tool = builtins.readFile ./shell/check-tool.sh;
local-dir = builtins.readFile ./shell/local-dir.sh;
load-sops = builtins.readFile ./shell/load-sops.sh;
};
sharedShellAliases = {
@@ -102,7 +130,7 @@ in {
# Consolidated SQL bundles for the `hectic` schema. Single source of truth
# for everything that creates objects in the `hectic` namespace, used by
# migrator (init-time) and db-tool (postgres-init + hydrate). Consumers apply
# migrator (init-time), db-dev/database hydrate, and db-ops secrets loading. Consumers apply
# the full bundle via lib/hook/apply-hectic-bundle.sh.
#
# The whole hectic system shares one `versionString`; `hectic-version.sql`
+4 -6
View File
@@ -10,8 +10,8 @@
# Usage:
# apply_hectic_bundle <PGURL> [<DOTENV_CONTENT>]
#
# If DOTENV_CONTENT is non-empty, it is loaded into hectic.secret via
# hectic.load_secrets_from_env() after the bundle is applied.
# If DOTENV_CONTENT is non-empty, it is base64-encoded and then loaded into
# hectic.secret via hectic.load_secrets_from_env() after the bundle is applied.
# SQL file paths are substituted by Nix evaluation time.
apply_hectic_bundle() {
@@ -41,11 +41,9 @@ apply_hectic_bundle() {
done
if [ -n "$env_content" ]; then
# Dollar-quote with $ps_env$ tag to preserve all content verbatim.
env_content_b64="$(printf '%s' "$env_content" | base64 | tr -d '\n')" || return 1
psql "$pgurl" -v ON_ERROR_STOP=1 <<SQL || return 1
SELECT hectic.load_secrets_from_env(\$ps_env\$
$env_content
\$ps_env\$);
SELECT hectic.load_secrets_from_env(convert_from(decode('$env_content_b64', 'base64'), 'UTF8'));
SQL
fi
+6 -5
View File
@@ -4,8 +4,8 @@ Single source of truth for every object created in the `hectic` PostgreSQL
schema. Consumed by:
- `package/migrator` — applies the bundle on `migrator init` (mandatory).
- `package/db-tool` — applies the bundle in `database hydrate` (default; opt
out with `--no-hook`).
- `package/db-tool` — applies the bundle in `db-dev` / `database hydrate`
(default; opt out with `--no-hook`) and in `db-ops secrets load`.
- External consumers (e.g. `proxydoe`) — invoke `psql -f` directly against the
paths exposed via `self.lib.hectic.*.path`.
@@ -60,7 +60,7 @@ verbatim source files in the store.
`lib/hook/apply-hectic-bundle.sh` is a dash-compatible helper template.
`self.lib.hectic.applyBundleScript` is the generated shell source with concrete
SQL paths embedded at Nix evaluation time. `migrator` and `db-tool` splice that
SQL paths embedded at Nix evaluation time. `migrator`, `db-dev`, and `db-ops` splice that
shell source directly into their generated scripts. Public entry point:
```sh
@@ -87,8 +87,9 @@ External consumers that do not want to source the helper can still invoke
4. Add a matching placeholder/replacement in `lib.hectic.applyBundleScript` and
update `lib/hook/apply-hectic-bundle.sh` to apply the file.
5. Bump `HECTIC_VERSION` if the new content changes existing semantics.
6. Update tests in `test/package/migrator/test/postgresql/init-hectic-bundle/`
and `test/package/db-tool/test/postgresql/hydrate-hook/`.
6. Update tests in `test/package/migrator/test/postgresql/init-hectic-bundle/`,
`test/package/db-tool/test/postgresql/hydrate-hook/`, and any `db-ops`
bundle-loading coverage.
## Versioning
+204
View File
@@ -0,0 +1,204 @@
load_sops_shell_quote() {
printf "'"
printf '%s' "$1" | sed "s/'/'\"'\"'/g"
printf "'"
}
load_sops_normalize_key() {
load_sops_normalized_key=$(printf '%s' "$1" | tr '.-' '__' | tr '[:lower:]' '[:upper:]')
case "$load_sops_normalized_key" in
''|[!A-Z_]*|*[!A-Z0-9_]*)
printf 'load-sops: invalid environment name after key normalization\n' >&2
return 1
;;
esac
printf '%s\n' "$load_sops_normalized_key"
}
load_sops_env_from_sops_file() {
load_sops_file=$1
load_sops_extract=${2-}
if ! command -v sops >/dev/null 2>&1; then
printf 'load-sops: required tool `sops` not found\n' >&2
return 1
fi
if ! command -v yq >/dev/null 2>&1; then
printf 'load-sops: required tool `yq` not found\n' >&2
return 1
fi
load_sops_decrypted=''
load_sops_attempt=0
load_sops_max_retries=${LOAD_SOPS_MAX_RETRIES:-1}
load_sops_prompt=${LOAD_SOPS_PROMPT:-0}
while :; do
if [ -n "$load_sops_extract" ]; then
if load_sops_decrypted=$(sops -d --extract "$load_sops_extract" "$load_sops_file" 2>/dev/null); then
load_sops_status=0
else
load_sops_status=$?
fi
else
if load_sops_decrypted=$(sops -d "$load_sops_file" 2>/dev/null); then
load_sops_status=0
else
load_sops_status=$?
fi
fi
if [ "$load_sops_status" -eq 0 ]; then
break
fi
if [ "$load_sops_prompt" != 1 ]; then
printf 'load-sops: failed to decrypt file\n' >&2
return 1
fi
if ! [ -t 0 ] || ! [ -r /dev/tty ]; then
printf 'load-sops: decrypt failed and prompt requested, but no TTY is available\n' >&2
return 1
fi
load_sops_attempt=$((load_sops_attempt + 1))
if [ "$load_sops_max_retries" != 0 ] && [ "$load_sops_attempt" -gt "$load_sops_max_retries" ]; then
printf 'load-sops: decrypt failed after configured retries\n' >&2
return 1
fi
load_sops_use_script=${LOAD_SOPS_USE_SCRIPT:-auto}
load_sops_quoted_file=$(load_sops_shell_quote "$load_sops_file") || return 1
load_sops_quoted_tty=$(load_sops_shell_quote "$(tty)") || return 1
case "$load_sops_use_script" in
auto)
if command -v script >/dev/null 2>&1 && [ -t 0 ]; then
if script -qefc "env GPG_TTY=$load_sops_quoted_tty sops --output /dev/null -d $load_sops_quoted_file" /dev/null >/dev/null 2>&1; then
load_sops_script_status=0
else
load_sops_script_status=$?
fi
if [ "$load_sops_script_status" -eq 0 ]; then
continue
fi
fi
;;
1)
if ! command -v script >/dev/null 2>&1; then
printf 'load-sops: required tool `script` not found\n' >&2
return 1
fi
if script -qefc "env GPG_TTY=$load_sops_quoted_tty sops --output /dev/null -d $load_sops_quoted_file" /dev/null >/dev/null 2>&1; then
load_sops_script_status=0
else
load_sops_script_status=$?
fi
if [ "$load_sops_script_status" -eq 0 ]; then
continue
fi
;;
0)
;;
*)
printf 'load-sops: LOAD_SOPS_USE_SCRIPT must be auto, 0, or 1\n' >&2
return 1
;;
esac
printf 'load-sops: enter SOPS_AGE_KEY_CMD: ' >/dev/tty
if ! IFS= read -r SOPS_AGE_KEY_CMD </dev/tty; then
printf 'load-sops: failed to read prompt input\n' >&2
return 1
fi
export SOPS_AGE_KEY_CMD
done
load_sops_env_from_yaml_text "$load_sops_decrypted"
}
load_sops_env_from_yaml_file() {
load_sops_file=$1
if ! command -v yq >/dev/null 2>&1; then
printf 'load-sops: required tool `yq` not found\n' >&2
return 1
fi
load_sops_env_from_yaml_text "$(cat "$load_sops_file")"
}
load_sops_env_from_yaml_text() {
load_sops_yaml=$1
load_sops_seen=''
if load_sops_keys=$(printf '%s' "$load_sops_yaml" | yq -r 'keys | .[]' 2>/dev/null); then
load_sops_keys_status=0
else
load_sops_keys_status=$?
fi
if [ "$load_sops_keys_status" -ne 0 ]; then
printf 'load-sops: failed to inspect YAML top-level keys\n' >&2
return 1
fi
while IFS= read -r load_sops_key; do
[ -n "$load_sops_key" ] || continue
load_sops_name=$(load_sops_normalize_key "$load_sops_key") || return 1
case "
$load_sops_seen
" in
*"
$load_sops_name
"*)
if [ "${LOAD_SOPS_ALLOW_COLLISIONS:-0}" != 1 ]; then
printf 'load-sops: normalized environment name collision\n' >&2
return 1
fi
;;
esac
load_sops_seen=${load_sops_seen}${load_sops_seen:+"
"}$load_sops_name
load_sops_kind=$(printf '%s' "$load_sops_yaml" | yq -r '."'"$load_sops_key"'" | kind' 2>/dev/null) || {
printf 'load-sops: failed to inspect YAML value kind\n' >&2
return 1
}
load_sops_tag=$(printf '%s' "$load_sops_yaml" | yq -r '."'"$load_sops_key"'" | tag' 2>/dev/null) || {
printf 'load-sops: failed to inspect YAML value tag\n' >&2
return 1
}
if [ "$load_sops_kind" != scalar ]; then
printf 'load-sops: top-level YAML values must be scalars\n' >&2
return 1
fi
if [ "$load_sops_tag" = '!!null' ]; then
printf 'load-sops: top-level YAML null values are not supported\n' >&2
return 1
fi
if [ "${LOAD_SOPS_OVERWRITE:-1}" = 0 ]; then
eval 'load_sops_already_set=${'"$load_sops_name"'+x}'
if [ -n "$load_sops_already_set" ]; then
continue
fi
fi
load_sops_value=$(printf '%s' "$load_sops_yaml" | yq -r '."'"$load_sops_key"'"' 2>/dev/null) || {
printf 'load-sops: failed to read YAML scalar value\n' >&2
return 1
}
load_sops_quoted=$(load_sops_shell_quote "$load_sops_value") || return 1
eval "export $load_sops_name=$load_sops_quoted"
done <<EOF
$load_sops_keys
EOF
}
+1
View File
@@ -250,6 +250,7 @@ in {
# failover flip does not need a separate provisioning step.
systemd.tmpfiles.rules = [
"d /var/lib/matrix-synapse 0750 matrix-synapse matrix-synapse -"
"Z ${s3Cfg.mediaStorePath} 0700 matrix-synapse matrix-synapse -"
];
systemd.services.matrix-cluster-signing-key = {
+52 -10
View File
@@ -1,7 +1,5 @@
{
inputs,
flake,
self,
...
}:
{
pkgs,
@@ -11,6 +9,13 @@
}: let
cfg = config.hectic.hardware.hetzner-cloud;
isNewer = cfg.generation == "newer";
networkMatchConfig =
(lib.optionalAttrs (cfg.networkMatchConfigName != null) {
Name = cfg.networkMatchConfigName;
})
// (lib.optionalAttrs (cfg.networkMatchConfigMac != null) {
PermanentMACAddress = cfg.networkMatchConfigMac;
});
in {
options.hectic.hardware.hetzner-cloud = {
enable = lib.mkEnableOption "Enable hetzner-cloud hardware configurations";
@@ -51,6 +56,14 @@ in {
'';
};
floatingIpv4 = lib.mkOption {
type = with lib.types; nullOr (strMatching "^([0-9]{1,3}\\.){3}[0-9]{1,3}$");
default = null;
example = "188.243.124.247";
description = ''
Optional Hetzner Floating IPv4 configured as `/32` on the primary interface.
'';
};
device = lib.mkOption {
type = lib.types.str;
default = if isNewer then "/dev/nvme0n1" else "/dev/sda";
@@ -65,14 +78,27 @@ in {
'';
};
networkMatchConfigName = lib.mkOption {
type = lib.types.strMatching "^(enp1s0|ens3|eth0)$";
type = with lib.types; nullOr str;
default = null;
example = "enp1s0";
description = ''
type of network conection,
on older hetzner servers may be `ens3` or else
on newer probably `enp1s0`
Optional interface name to match in systemd-networkd.
you can use `networkctl list` on server to know it
Prefer `networkMatchConfigMac` for stable matching across rescue
images and installed systems that may rename interfaces differently.
You can use `networkctl list` on server to know it.
'';
};
networkMatchConfigMac = lib.mkOption {
type = with lib.types; nullOr (strMatching "^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$");
default = null;
example = "92:00:08:4a:b0:32";
description = ''
Optional permanent MAC address to match in systemd-networkd.
This is the preferred Hetzner Cloud matching method because interface
names can differ between rescue images and installed NixOS systems.
'';
};
};
@@ -80,6 +106,15 @@ in {
config = lib.mkIf cfg.enable (lib.mkMerge
[
{
boot.loader.systemd-boot.enable = false;
boot.loader.efi.canTouchEfiVariables = false;
boot.loader.grub = {
enable = true;
efiSupport = true;
efiInstallAsRemovable = true;
device = "nodev";
};
boot.initrd.availableKernelModules = [
"ata_piix"
"uhci_hcd"
@@ -90,12 +125,12 @@ in {
networking.useNetworkd = true;
systemd.network.enable = true;
systemd.network.networks."30-wan" = {
matchConfig.Name = cfg.networkMatchConfigName;
matchConfig = networkMatchConfig;
networkConfig.DHCP = "no";
address = [
"${cfg.ipv4}/32"
"${cfg.ipv6}::/64"
];
] ++ lib.optional (cfg.floatingIpv4 != null) "${cfg.floatingIpv4}/32";
routes = [
{ Gateway = "172.31.1.1"; GatewayOnLink = true; }
{ Gateway = "fe80::1"; }
@@ -137,6 +172,13 @@ in {
};
};
};
assertions = [
{
assertion = cfg.networkMatchConfigName != null || cfg.networkMatchConfigMac != null;
message = "hectic.hardware.hetzner-cloud requires networkMatchConfigName or networkMatchConfigMac";
}
];
}
(lib.mkIf (pkgs.stdenv.hostPlatform.system == "aarch64-linux") {
boot.initrd.kernelModules = [ "virtio_gpu" ];
+134
View File
@@ -0,0 +1,134 @@
{ ... }:
{
lib,
config,
...
}: let
cfg = config.hectic.hardware.njalla;
in {
options.hectic.hardware.njalla = {
enable = lib.mkEnableOption "Enable njalla hardware configurations";
ipv4 = lib.mkOption {
type = lib.types.strMatching "^([0-9]{1,3}\\.){3}[0-9]{1,3}$";
example = "185.193.126.103";
description = ''
Njalla IPv4 address assigned to the host.
'';
};
ipv4PrefixLength = lib.mkOption {
type = lib.types.int;
default = 24;
example = 24;
description = ''
Njalla IPv4 prefix length.
'';
};
ipv4Gateway = lib.mkOption {
type = lib.types.strMatching "^([0-9]{1,3}\\.){3}[0-9]{1,3}$";
default = "185.193.126.1";
example = "185.193.126.1";
description = ''
Njalla IPv4 gateway.
'';
};
ipv6 = lib.mkOption {
type = lib.types.strMatching "^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$";
example = "2a0a:3840:1337:126:0:b9c1:7e67:1337";
description = ''
Njalla IPv6 address assigned to the host.
'';
};
ipv6PrefixLength = lib.mkOption {
type = lib.types.int;
default = 64;
example = 64;
description = ''
Njalla IPv6 prefix length.
'';
};
ipv6Gateway = lib.mkOption {
type = lib.types.strMatching "^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$";
default = "2a0a:3840:1337:126::1";
example = "2a0a:3840:1337:126::1";
description = ''
Njalla IPv6 gateway.
'';
};
networkMatchConfigName = lib.mkOption {
type = lib.types.str;
default = "eth0";
example = "eth0";
description = ''
Njalla container network interface name.
'';
};
device = lib.mkOption {
type = lib.types.str;
default = "/dev/vda";
example = "/dev/disk/by-id/virtio-root";
description = ''
Njalla installation disk for disko/nixos-anywhere.
`/dev/vda` is the default block device visible on the inspected Njalla
host. Prefer a stable `/dev/disk/by-id/...` path when available.
'';
};
enableDisko = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to provide a disko layout for nixos-anywhere installs.
'';
};
};
config = lib.mkIf cfg.enable (lib.mkMerge [
{
boot.isContainer = true;
networking.useDHCP = false;
networking.useNetworkd = true;
systemd.network.enable = true;
systemd.network.networks."30-wan" = {
matchConfig.Name = cfg.networkMatchConfigName;
networkConfig.DHCP = "no";
address = [
"${cfg.ipv4}/${toString cfg.ipv4PrefixLength}"
"${cfg.ipv6}/${toString cfg.ipv6PrefixLength}"
];
routes = [
{ Gateway = cfg.ipv4Gateway; }
{ Gateway = cfg.ipv6Gateway; }
];
};
networking.nameservers = [ "1.1.1.1" "8.8.8.8" ];
}
(lib.mkIf cfg.enableDisko {
boot.loader.grub.device = cfg.device;
disko.devices.disk.main = {
type = "disk";
device = cfg.device;
content = {
type = "gpt";
partitions = {
boot = {
size = "1M";
type = "EF02";
priority = 1;
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
})
]);
}
+1 -30
View File
@@ -25,36 +25,7 @@ in {
programs.bash.shellAliases.tmux = "tmux a";
home-manager.sharedModules = [
{
programs.tmux = {
enable = true;
plugins = with pkgs.tmuxPlugins; [ resurrect continuum ];
keyMode = "vi";
escapeTime = 500;
historyLimit = 50000;
newSession = true;
extraConfig = ''
# resurrect
set -g @resurrect-strategy-vim 'session'
set -g @resurrect-strategy-nvim 'session'
set -g @resurrect-capture-pane-contents 'on'
resurrect_dir="$HOME/.tmux/resurrect"
set -g @resurrect-dir $resurrect_dir
set -g @resurrect-hook-post-save-all 'target=$(readlink -f $resurrect_dir/last); sed "s| --cmd .*-vim-pack-dir||g; s|/etc/profiles/per-user/$USER/bin/||g; s|/home/$USER/.nix-profile/bin/||g" $target | sponge $target'
# continuum
set -g @continuum-restore 'on'
set -g @continuum-boot 'on'
set -g @continuum-save-interval '10'
bind-key -T copy-mode-vi v send-keys -X begin-selection
bind-key -T copy-mode-vi C-v send-keys -X rectangle-toggle
bind-key O select-pane -t :.-
'';
};
}
(flake + "/home/module/program/tmux.nix")
];
home-manager.users.root.home.stateVersion = lib.mkDefault "25.05";
@@ -0,0 +1,153 @@
{ ... }: {
pkgs,
lib,
config,
...
}: let
cfg = config.hectic.services.stable-video-diffusion;
in {
options.hectic.services.stable-video-diffusion = {
enable = lib.mkEnableOption "local Stable Video Diffusion HTTP API";
host = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "Address the Stable Video Diffusion API binds to.";
};
port = lib.mkOption {
type = lib.types.port;
default = 7861;
description = "Port the Stable Video Diffusion API binds to.";
};
package = lib.mkOption {
type = lib.types.package;
default = pkgs.hectic.stable-video-diffusion-api;
defaultText = lib.literalExpression "pkgs.hectic.stable-video-diffusion-api";
description = "Package providing the Stable Video Diffusion API executable.";
};
modelId = lib.mkOption {
type = lib.types.str;
default = "stabilityai/stable-video-diffusion-img2vid-xt";
description = "Model identifier loaded by the Stable Video Diffusion API.";
};
device = lib.mkOption {
type = with lib.types; nullOr (enum [ "cpu" "cuda" ]);
default = null;
description = ''
Torch device requested from the Stable Video Diffusion API. When null,
the package keeps its own auto-detection behavior.
'';
};
libraryPath = lib.mkOption {
type = with lib.types; listOf str;
default = [];
description = ''
Runtime library paths added to LD_LIBRARY_PATH. CUDA/NVIDIA deployments
should include /run/opengl-driver/lib so libcuda.so is visible to torch.
'';
};
stateDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/stable-video-diffusion-api";
description = "Persistent state directory for the Stable Video Diffusion API.";
};
cacheDir = lib.mkOption {
type = lib.types.str;
default = "/var/cache/stable-video-diffusion-api";
description = "Cache directory for downloaded model and runtime artifacts.";
};
outputDir = lib.mkOption {
type = lib.types.str;
default = "${cfg.stateDir}/outputs";
defaultText = lib.literalExpression ''"\${config.hectic.services.stable-video-diffusion.stateDir}/outputs"'';
description = "Directory where generated video outputs are written.";
};
environmentFile = lib.mkOption {
type = with lib.types; nullOr path;
default = null;
description = ''
Optional environment file for secrets or runtime overrides. Values from
the unit environment define SVD_API_HOST, SVD_API_PORT, SVD_MODEL_ID,
SVD_STATE_DIR, SVD_CACHE_DIR, SVD_OUTPUT_DIR, and optionally SVD_DEVICE
and LD_LIBRARY_PATH by default.
'';
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to open the API port in the firewall.";
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.device != "cuda" || cfg.libraryPath != [];
message = "hectic.services.stable-video-diffusion.libraryPath must include the NVIDIA runtime library path when device is cuda.";
}
];
users.users.stable-video-diffusion = {
isSystemUser = true;
group = "stable-video-diffusion";
};
users.groups.stable-video-diffusion = {};
systemd.tmpfiles.rules = [
"d ${cfg.stateDir} 0750 stable-video-diffusion stable-video-diffusion -"
"d ${cfg.cacheDir} 0750 stable-video-diffusion stable-video-diffusion -"
"d ${cfg.outputDir} 0750 stable-video-diffusion stable-video-diffusion -"
];
systemd.services.stable-video-diffusion-api = {
description = "Stable Video Diffusion HTTP API";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
SVD_API_HOST = cfg.host;
SVD_API_PORT = toString cfg.port;
SVD_MODEL_ID = cfg.modelId;
SVD_STATE_DIR = cfg.stateDir;
SVD_CACHE_DIR = cfg.cacheDir;
SVD_OUTPUT_DIR = cfg.outputDir;
}
// lib.optionalAttrs (cfg.device != null) {
SVD_DEVICE = cfg.device;
}
// lib.optionalAttrs (cfg.libraryPath != []) {
LD_LIBRARY_PATH = lib.concatStringsSep ":" cfg.libraryPath;
};
serviceConfig = lib.mkMerge [
{
Type = "simple";
User = "stable-video-diffusion";
Group = "stable-video-diffusion";
WorkingDirectory = cfg.stateDir;
ExecStart = lib.getExe' cfg.package "stable-video-diffusion-api";
Restart = "on-failure";
RestartSec = "5s";
TimeoutStopSec = "30s";
KillSignal = "SIGTERM";
KillMode = "mixed";
StandardOutput = "journal";
StandardError = "journal";
}
(lib.mkIf (cfg.environmentFile != null) {
EnvironmentFile = cfg.environmentFile;
})
];
};
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.port ];
};
}
@@ -1,230 +0,0 @@
# Matrix Cluster Failover Runbook (`accord.tube`)
Primary: `hectic-lab` (NL, `128.140.75.58`)
Standby: `bfs.poland.xray` (PL, `91.198.166.181`)
Module: `hectic.generic.matrix-cluster` (`nixos/module/generic/matrix-cluster.nix`).
Shared secrets: `sus/matrix-cluster.yaml`.
All `psql` and `pg_ctl` invocations use PostgreSQL **17** at data dir
`/var/lib/postgresql/17`.
## Initial setup
### 1. Provision shared SOPS file (`sus/matrix-cluster.yaml`)
On a workstation with both yukkop and yukkop-alt age keys available:
```sh
sudo cat /var/lib/matrix-synapse/homeserver.signing.key # on NL (hectic-lab)
# Copy the single line value into the buffer for the next step.
sops sus/matrix-cluster.yaml
```
Populate the editor with:
```yaml
matrix:
signing-key: <paste verbatim signing-key line from NL>
postgres-replication-password: <openssl rand -base64 32>
object-storage:
credentials: |
ACCESS_KEY_ID=<verbatim copy from sus/hectic-lab.yaml>
SECRET_ACCESS_KEY=<verbatim copy from sus/hectic-lab.yaml>
porkbun-api-key: <PORKBUN_API_KEY>
porkbun-secret-api-key: <PORKBUN_SECRET_API_KEY>
```
Verify recipients:
```sh
sops updatekeys sus/matrix-cluster.yaml
sops -d sus/matrix-cluster.yaml | grep -E 'signing-key|porkbun-api-key|object-storage'
```
Expected: all five keys present, exit 0.
### 2. Deploy NL primary first
```sh
nixos-rebuild switch --flake .#'hectic-lab|x86_64-linux' --target-host root@128.140.75.58
```
Verify on NL:
```sh
sudo systemctl status matrix-synapse postgresql matrix-cluster-replication-password
sudo -u postgres psql -c "select rolname, rolreplication from pg_roles where rolname='replication';"
# Expected: replication | t
```
### 3. Seed PL replica with `pg_basebackup`
On PL:
```sh
sudo systemctl stop postgresql
sudo rm -rf /var/lib/postgresql/17
sudo -u postgres install -d -m 0700 /var/lib/postgresql/17
sudo -u postgres PGPASSWORD="$(sudo cat /run/secrets/matrix/postgres-replication-password)" \
pg_basebackup \
-h 128.140.75.58 \
-p 5432 \
-U replication \
-D /var/lib/postgresql/17 \
-Fp -Xs -P -R \
--no-password
```
`-R` writes `standby.signal` and an initial `primary_conninfo`. The
matrix-cluster module's `matrix-cluster-standby-bootstrap` service will
overwrite `primary_conninfo` to use a libpq passfile on next boot.
### 4. Deploy PL standby
```sh
nixos-rebuild switch --flake .#'bfs.poland.xray|x86_64-linux' --target-host root@91.198.166.181
sudo systemctl start postgresql
```
Verify streaming on NL:
```sh
sudo -u postgres psql -c 'select client_addr, state, sync_state from pg_stat_replication;'
# Expected: 91.198.166.181 | streaming | async
```
Verify standby on PL:
```sh
sudo -u postgres psql -c 'select pg_is_in_recovery();'
# Expected: t
sudo systemctl is-active matrix-synapse
# Expected: inactive (standby keeps Synapse off)
```
### 5. Remove duplicate S3 credentials from `sus/hectic-lab.yaml`
Only AFTER NL is confirmed healthy reading from the new shared file:
```sh
sops sus/hectic-lab.yaml
# Delete the matrix/object-storage/credentials block.
sudo nixos-rebuild switch --flake .#'hectic-lab|x86_64-linux'
```
## Normal operations
```sh
# NL: replication health
sudo -u postgres psql -c 'select * from pg_stat_replication;'
# Expected: 1 row, state=streaming, sync_state=async
# PL: replay status
sudo -u postgres psql -c 'select now() - pg_last_xact_replay_timestamp() as lag;'
# Both: cert renewal
sudo systemctl status acme-accord.tube.timer
sudo journalctl -u acme-accord.tube.service --since '24 hours ago'
# Synapse health (NL primary)
curl -sf https://accord.tube/_matrix/client/versions | head
```
## Planned failover (NL -> PL)
```sh
# 1. Drain NL: stop accepting writes.
sudo systemctl stop matrix-synapse
sudo systemctl stop postgresql # ensure no new WAL after this point
# 2. Promote PL replica.
sudo -u postgres pg_ctl -D /var/lib/postgresql/17 promote
# Wait until pg_is_in_recovery() returns f:
sudo -u postgres psql -c 'select pg_is_in_recovery();'
# 3. Make the role switch declarative before rebuilding.
# Edit the flake so rebuilds match the promoted database state:
# - nixos/system/bfs.poland.xray/bfs.poland.xray.nix:
# hectic.generic.matrix-cluster.role = "primary";
# hectic.generic.matrix-cluster.overrideEnableSynapse = true;
# hectic.generic.matrix-cluster.secretsFile = config.sops.secrets."matrix/secrets".path;
# - nixos/system/hectic-lab/hectic-lab.nix:
# hectic.generic.matrix-cluster.role = "standby";
# hectic.generic.matrix-cluster.overrideEnableSynapse = false;
# hectic.generic.matrix-cluster.replication.peerHost = "91.198.166.181";
# hectic.generic.matrix-cluster.replication.allowedSourceIPs = [ "128.140.75.58/32" ];
# (You will also need a matrix/secrets entry on PL - copy from NL via SOPS.)
sudo nixos-rebuild switch --flake .#'bfs.poland.xray|x86_64-linux'
sudo nixos-rebuild switch --flake .#'hectic-lab|x86_64-linux'
sudo systemctl status matrix-synapse
# 4. Swap DNS A record at Porkbun:
# accord.tube A 91.198.166.181 (was 128.140.75.58)
# TTL: set to 300 in advance of any planned failover.
# Porkbun UI: https://porkbun.com/account/domainsSpeedy -> accord.tube -> DNS -> edit A record.
# Or via API:
sudo curl -sX POST https://api.porkbun.com/api/json/v3/dns/editByNameType/accord.tube/A \
-H 'content-type: application/json' \
-d "$(jq -n --arg k "$PORKBUN_API_KEY" --arg s "$PORKBUN_SECRET_API_KEY" \
'{secretapikey:$s,apikey:$k,content:"91.198.166.181",ttl:"300"}')"
# 5. Federation smoke test.
curl -s 'https://federationtester.matrix.org/api/report?server_name=accord.tube' | jq .FederationOK
# Expected: true
```
Expected after the rebuilds:
- `bfs.poland.xray` evaluates and runs as `role = "primary"`.
- `hectic-lab` evaluates as `role = "standby"` with Synapse forced off.
- Future `nixos-rebuild` runs preserve the promoted topology instead of reapplying standby settings to PL.
## Failback (PL -> NL)
```sh
# 1. Stop NL postgres if still up; clear its data dir.
sudo systemctl stop postgresql matrix-synapse
sudo rm -rf /var/lib/postgresql/17
# 2. Re-seed NL from PL (now the live primary).
sudo -u postgres install -d -m 0700 /var/lib/postgresql/17
sudo -u postgres PGPASSWORD="$(sudo cat /run/secrets/matrix/postgres-replication-password)" \
pg_basebackup -h 91.198.166.181 -p 5432 -U replication \
-D /var/lib/postgresql/17 -Fp -Xs -P -R --no-password
# 3. Temporarily flip roles in the flake:
# - hectic-lab.nix: role = "standby"; peerHost = "91.198.166.181";
# - bfs.poland.xray.nix: role = "primary"; peerHost = "128.140.75.58";
# Rebuild both.
# 4. Once NL is streaming green, do the reverse failover dance:
sudo systemctl stop matrix-synapse # on PL
sudo -u postgres pg_ctl -D /var/lib/postgresql/17 promote # on NL
# Then revert the flake role assignments back to NL=primary / PL=standby and
# rebuild both hosts.
# 5. Swap DNS back at Porkbun (A -> 128.140.75.58).
```
## Disaster recovery (NL permanently lost)
```sh
# 1. Promote PL as the new permanent primary.
sudo -u postgres pg_ctl -D /var/lib/postgresql/17 promote
# 2. Edit nixos/system/bfs.poland.xray/bfs.poland.xray.nix:
# hectic.generic.matrix-cluster.role = "primary";
# hectic.generic.matrix-cluster.overrideEnableSynapse = lib.mkForce null;
# hectic.generic.matrix-cluster.replication.peerHost = "<new-standby-ip>";
# hectic.generic.matrix-cluster.replication.allowedSourceIPs = [ "<new-standby-ip>/32" ];
# 3. Provision a new host (any region with Porkbun-managed DNS) and import
# self.nixosModules.matrix-cluster with role = "standby" pointed at PL's IP.
# 4. Bootstrap the new standby via pg_basebackup from PL exactly as in
# "Initial setup" step 3, replacing 128.140.75.58 with PL's IP.
# 5. Update Porkbun A record to PL's IP permanently.
```
@@ -9,17 +9,11 @@
config,
...
}: let
matrixBackend = "https://128.140.75.58";
matrixHost = "accord.tube";
jitsiHost = "meet.accord.tube";
elementEntryDomain = "element.bfs.band";
polandEntryDomain = "bfs.band";
matrixClusterSopsFile = flake + "/sus/matrix-cluster.yaml";
backendProxyConfig = ''
proxy_ssl_server_name on;
proxy_ssl_name ${matrixHost};
proxy_set_header Host ${matrixHost};
'';
in {
imports = [
self.nixosModules.xray-system
@@ -47,7 +41,6 @@ in {
credentialsFile = config.sops.secrets."matrix/object-storage/credentials".path;
};
replication = {
peerHost = "128.140.75.58";
passwordFile = config.sops.secrets."matrix/postgres-replication-password".path;
};
acme = {
@@ -72,6 +65,14 @@ in {
hostName = jitsiHost;
};
# NOTE(yukkop): disk was provisioned outside disko, so the expected partition
# label does not exist. Pin root to the live filesystem UUID so stage 1 can
# mount `/` reliably.
fileSystems."/" = lib.mkForce {
device = "/dev/disk/by-uuid/06b48ef1-a1eb-428d-821c-90c96a624542";
fsType = "ext4";
};
security.acme = {
acceptTerms = true;
defaults.email = "security@bfs.band";
@@ -125,19 +126,17 @@ in {
};
locations."= /livekit/jwt" = {
proxyPass = "${matrixBackend}/livekit/jwt";
extraConfig = backendProxyConfig;
proxyPass = "http://[::1]:${toString config.services.lk-jwt-service.port}/";
};
locations."^~ /livekit/jwt/" = {
proxyPass = "${matrixBackend}/livekit/jwt/";
extraConfig = backendProxyConfig;
proxyPass = "http://[::1]:${toString config.services.lk-jwt-service.port}/";
};
locations."= /livekit/sfu" = {
proxyPass = "${matrixBackend}/livekit/sfu";
proxyPass = "http://[::1]:${toString config.services.livekit.settings.port}/";
proxyWebsockets = true;
extraConfig = backendProxyConfig + ''
extraConfig = ''
proxy_send_timeout 120;
proxy_read_timeout 120;
proxy_buffering off;
@@ -148,9 +147,9 @@ in {
};
locations."^~ /livekit/sfu/" = {
proxyPass = "${matrixBackend}/livekit/sfu/";
proxyPass = "http://[::1]:${toString config.services.livekit.settings.port}/";
proxyWebsockets = true;
extraConfig = backendProxyConfig + ''
extraConfig = ''
proxy_send_timeout 120;
proxy_read_timeout 120;
proxy_buffering off;
@@ -161,13 +160,14 @@ in {
};
locations."^~ /_matrix/" = {
proxyPass = "${matrixBackend}/_matrix/";
extraConfig = backendProxyConfig;
proxyPass = "http://127.0.0.1:8008";
extraConfig = ''
client_max_body_size ${config.hectic.generic.matrix-cluster.maxUploadSize};
'';
};
locations."^~ /_synapse/client/" = {
proxyPass = "${matrixBackend}/_synapse/client/";
extraConfig = backendProxyConfig;
proxyPass = "http://127.0.0.1:8008";
};
};
@@ -0,0 +1,74 @@
{ pkgs, ... }:
let
port = 22222;
stateDir = "/var/lib/experimental-sshd";
configFile = pkgs.writeText "experimental-sshd_config" ''
Port 22222
ListenAddress 0.0.0.0
ListenAddress ::
HostKey ${stateDir}/ssh_host_ed25519_key
HostKey ${stateDir}/ssh_host_rsa_key
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
PermitRootLogin prohibit-password
UsePAM yes
AuthenticationMethods publickey
AuthorizedKeysFile %h/.ssh/authorized_keys /etc/ssh/authorized_keys.d/%u
LogLevel DEBUG3
VersionAddendum none
HostKeyAlgorithms rsa-sha2-512,rsa-sha2-256,ssh-ed25519
KexAlgorithms curve25519-sha256,diffie-hellman-group14-sha256
Ciphers aes256-ctr,aes128-ctr
MACs hmac-sha2-512,hmac-sha2-256
'';
keygenScript = pkgs.writeShellScript "experimental-sshd-keygen" ''
set -eu
${pkgs.coreutils}/bin/mkdir -p -m 0700 ${stateDir}
if [ ! -f ${stateDir}/ssh_host_ed25519_key ]; then
${pkgs.openssh}/bin/ssh-keygen -t ed25519 -f ${stateDir}/ssh_host_ed25519_key -N ""
fi
if [ ! -f ${stateDir}/ssh_host_rsa_key ]; then
${pkgs.openssh}/bin/ssh-keygen -t rsa -b 4096 -f ${stateDir}/ssh_host_rsa_key -N ""
fi
'';
in
{
environment.etc."ssh/experimental-sshd_config".source = configFile;
networking.firewall.allowedTCPPorts = [ port ];
systemd.services.experimental-sshd-keygen = {
description = "Generate experimental SSH host keys";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
StateDirectory = "experimental-sshd";
ExecStart = keygenScript;
};
};
systemd.services.experimental-sshd = {
description = "Experimental SSH daemon";
wantedBy = [ "multi-user.target" ];
wants = [ "experimental-sshd-keygen.service" ];
after = [ "network.target" "experimental-sshd-keygen.service" ];
serviceConfig = {
Type = "simple";
StateDirectory = "experimental-sshd";
RuntimeDirectory = "experimental-sshd";
ExecStart = "${pkgs.openssh}/bin/sshd -D -e -f /etc/ssh/experimental-sshd_config";
};
preStart = ''
${pkgs.openssh}/bin/sshd -t -f /etc/ssh/experimental-sshd_config
'';
};
}
+10 -3
View File
@@ -15,7 +15,7 @@ with builtins;
with lib;
let
domain = "hectic-lab.com";
matrixDomain = "accord.tube";
sshPort = 22;
mailUserNames = [
"security"
"founders"
@@ -57,6 +57,7 @@ in {
(import ./attic.nix { inherit flake self inputs domain; })
(import ./containers.nix { inherit flake self inputs; })
./experimental-sshd.nix
(import ./ente.nix { inherit domain; })
(import ./mechabellum.nix { inherit flake self inputs domain; })
(import (./. + "/sentinèlla.nix") { inherit flake self inputs domain; })
@@ -80,6 +81,7 @@ in {
enable = true;
networkMatchConfigName = "enp1s0";
ipv4 = "128.140.75.58";
floatingIpv4 = "78.47.243.0";
ipv6 = "2a01:4f8:c2c:d54a";
};
services.matrix = {
@@ -106,6 +108,7 @@ in {
'';
environment.systemPackages = with pkgs; [
tcpdump
git
rsync
python311
@@ -133,6 +136,8 @@ in {
};
users.users.root.openssh.authorizedKeys.keys = [
# neuro machine
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDfqSROY+rp7amPPiArY3sZM7jTjYBS02csWxF/NeIr/ root@neuro"
# yukkop
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMuP5NSfEQmO6m77xBWZvZ3hk7cw1q2k2vbsFd37rybU u0_a327@localhost"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJBLxMo5icX2Xyng7mcWGnIi+c4ZbVygjPhuU8noCkfZ"
@@ -159,6 +164,8 @@ in {
];
};
services.openssh.ports = [ sshPort ];
services.mailserver = {
enable = true;
domain = domain;
@@ -179,10 +186,10 @@ in {
networking.firewall = {
allowedTCPPorts = [
sshPort # ssh
80
443
3306 # mysql
11012 # gitea ssh
25565
55228 # ss-bfs
];
@@ -264,7 +271,7 @@ in {
settings.service.DISABLE_REGISTRATION = true;
settings.server = {
HTTP_PORT = 11011;
#SSH_PORT = 22;
SSH_PORT = sshPort;
SSH_DOMAIN = "hectic-lab.com";
};
database = {
+2 -22
View File
@@ -15,32 +15,12 @@ in self.lib.nixpkgs-lib.nixosSystem {
self.overlays.default
inputs.nix-minecraft.overlay
];
config.allowUnfreePredicate = pkg: builtins.elem (self.lib.nixpkgs-lib.getName pkg) [
config.allowUnfreePredicate = pkg:
self.lib.cudaUnfreePredicate pkg || builtins.elem (self.lib.nixpkgs-lib.getName pkg) [
"minecraft-server"
"neoforge"
"nvidia-x11"
"cuda_nvcc"
"cuda_cudart"
"cuda_cuobjdump"
"cuda_cupti"
"cuda_nvdisasm"
"cuda_cccl"
"cuda_nvml_dev"
"cuda_nvrtc"
"cuda_nvtx"
"cuda_profiler_api"
"libcusparse_lt"
"libcublas"
"libcufft"
"libcufile"
"libcurand"
"libcusolver"
"libnvjitlink"
"libcusparse"
"cudnn"
];
# jitsi-meet depends on libolm which is marked insecure (CVE-2024-4519x)
config.permittedInsecurePackages = [
+18
View File
@@ -17,6 +17,11 @@
ollamaServiceBundledLibraryPath = "${ollamaPrebuilt}/lib/ollama:${ollamaPrebuilt}/lib/ollama/cuda_v12:${ollamaPrebuilt}/lib/ollama/cuda_v13";
stableVideoDiffusionLibraryPath = lib.makeLibraryPath [
pkgs.stdenv.cc.cc.lib
pkgs.zlib
];
ollamaPrebuilt = pkgs.stdenvNoCC.mkDerivation {
pname = "ollama";
version = "0.22.1";
@@ -176,6 +181,19 @@ in {
openFirewall = false;
};
hectic.services.stable-video-diffusion = {
enable = true;
host = "127.0.0.1";
port = 7861;
package = pkgs.hectic.stable-video-diffusion-api;
device = "cuda";
libraryPath = [
stableVideoDiffusionLibraryPath
"/run/opengl-driver/lib"
];
openFirewall = false;
};
networking = {
networkmanager.enable = true;
useDHCP = lib.mkDefault true;
+20
View File
@@ -0,0 +1,20 @@
{
flake,
self,
inputs,
system ? "x86_64-linux",
...
}: let
# Use folder name as name of this system
name = builtins.baseNameOf ./.;
in self.lib.nixpkgs-lib.nixosSystem {
pkgs = import inputs.nixpkgs {
inherit system;
overlays = [ self.overlays.default ];
};
modules = [
{ networking.hostName = name; }
(import ./${name}.nix { inherit flake self inputs; })
];
}
+35
View File
@@ -0,0 +1,35 @@
{
inputs,
self,
...
}: {
pkgs,
modulesPath,
...
}: {
imports = [
(modulesPath + "/profiles/qemu-guest.nix")
inputs.sops-nix.nixosModules.sops
self.nixosModules.hectic
];
hectic = {
archetype.dev.enable = true;
hardware.hetzner-cloud = {
enable = true;
device = "/dev/sda";
networkMatchConfigMac = "92:00:08:4a:b0:32";
ipv4 = "46.225.237.218";
ipv6 = "2a01:4f8:c2c:3b14";
};
};
users.users.root.openssh.authorizedKeys.keys = [
''ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKAaObjLBslsdTlqEcYaS1TqX4x9aVJu75y27/8MFevO''
];
environment.systemPackages = with pkgs; [
git
rsync
];
}
+13 -8
View File
@@ -1,18 +1,21 @@
# db-tool
PostgreSQL development database management tool. Drop-in replacement for per-project database.sh / postgres-init.sh / postgres-cleanup.sh scripts. Provides database, postgres-init, and postgres-cleanup binaries.
PostgreSQL utility package exposing separate development and operations binaries.
`db-dev` preserves the current development workflow via the `database` binary;
`db-ops` provides target-safe operational helpers such as secrets hydration.
## Provided Binaries
| Binary | Description |
| --- | --- |
| `database` | Main script for managing migrations, deployments, and logs. |
| `database` | Main `db-dev` script for managing local development databases, migrations, deployments, and logs. |
| `db-ops` | Operational helper binary for target/runtime database actions. |
| `postgres-init` | Ephemeral PostgreSQL cluster initialization and startup. |
| `postgres-cleanup` | Graceful shutdown and cleanup of the PostgreSQL cluster. |
## Required Environment Variables
These variables must be set for `db-tool` to function.
These variables must be set for `db-dev` / `database` to function.
| Variable | Description |
| --- | --- |
@@ -39,7 +42,7 @@ These variables must be set for `db-tool` to function.
## Postgres Package Override
By default, `db-tool`/`postgres-init`/`postgres-cleanup` use plain `postgresql_17` from nixpkgs. If you need extensions (e.g. `pg_cron`), override the postgres package per-output:
By default, `db-dev`/`db-ops`/`postgres-init`/`postgres-cleanup` use plain `postgresql_17` from nixpkgs. If you need extensions (e.g. `pg_cron`), override the postgres package per-output:
```nix
let
@@ -48,7 +51,8 @@ let
]);
in {
packages = [
(pkgs.hectic."db-tool".override { postgresql = myPg; })
(pkgs.hectic."db-dev".override { postgresql = myPg; })
(pkgs.hectic."db-ops".override { postgresql = myPg; })
(pkgs.hectic."postgres-init".override { postgresql = myPg; })
(pkgs.hectic."postgres-cleanup".override { postgresql = myPg; })
];
@@ -102,14 +106,14 @@ used by `database restore`.
## shellHook Example
To use `db-tool` in a Nix development shell, add the following to your `flake.nix` or `shell.nix`:
To use `db-dev` in a Nix development shell, add the following to your `flake.nix` or `shell.nix`:
```nix
{
# ...
devShells.default = pkgs.mkShell {
packages = [
pkgs.hectic.db-tool
pkgs.hectic.db-dev
pkgs.hectic.postgres-init
pkgs.hectic.postgres-cleanup
];
@@ -134,7 +138,7 @@ To use `db-tool` in a Nix development shell, add the following to your `flake.ni
## hectic Bundle
`db-tool` and `migrator` apply a single bundle of SQL files that bootstrap the
`db-dev`, `db-ops`, and `migrator` apply a single bundle of SQL files that bootstrap the
`hectic` schema. The bundle lives in
[`lib/hook/sql/`](../../lib/hook/sql/README.md) — see that README for full
contract, file layout, and the `self.lib.hectic.*` Nix API.
@@ -190,6 +194,7 @@ ALTER DATABASE mydb SET hectic.inheritance_extra_excluded_schemas = 'legacy,etl'
| `postgres-init` | **No.** Pure PostgreSQL provisioner — starts a vanilla cluster, nothing more. |
| `migrator init` | **Yes, mandatory.** The bundle is a hard prerequisite for `hectic.migration`. |
| `database hydrate` | **Yes, by default.** Re-applied on every hydrate. Skip with `--no-hook`. After applying the bundle, hydrate also calls `hectic.load_secrets_from_env(<dotenv>)` if `HECTIC_DOTENV_FILE` (or `${LOCAL_DIR}/.env.${ENVIRONMENT}`) is readable. |
| `db-ops secrets load` | **Yes, mandatory.** Applies the bundle and requires a dotenv source before loading secrets into `hectic.secret`. |
The bundle is idempotent — repeated application is safe.
+222
View File
@@ -0,0 +1,222 @@
# shellcheck shell=dash
: "${SCRIPT_NAME:=$(basename "$0")}"
help() {
# shellcheck disable=SC2059
printf "$(cat <<EOF
${BGREEN}Usage:${NC} $SCRIPT_NAME [OPTIONS] <SUBCOMMAND> [OPTIONS]
PostgreSQL operations utility.
${BGREEN}Global Options:${NC}
${BCYAN}-h${NC}, ${BCYAN}--help${NC} Show this help message
${BCYAN}-u${NC}, ${BCYAN}--url${NC} <url> PostgreSQL connection string
${BGREEN}Subcommands:${NC}
${BCYAN}secrets load${NC} [OPTIONS] Apply hectic bundle and load secrets
${BGREEN}Environment:${NC}
${BBLACK}PGURL${NC} PostgreSQL connection string fallback
${BBLACK}DB_URL${NC} PostgreSQL connection string fallback
${BBLACK}HECTIC_DOTENV_CONTENT${NC} Raw dotenv content to load
${BBLACK}HECTIC_DOTENV_FILE${NC} Dotenv file to read when readable
${BBLACK}LOCAL_DIR${NC} Used with ENVIRONMENT for .env fallback
${BBLACK}ENVIRONMENT${NC} Falls back to ${BBLACK}\$LOCAL_DIR/.env.\$ENVIRONMENT${NC}
EOF
)"
}
help_secrets_load() {
# shellcheck disable=SC2059
printf "$(cat <<EOF
${BGREEN}Usage:${NC} $SCRIPT_NAME ${BCYAN}secrets load${NC} [OPTIONS]
Apply the hectic SQL bundle and load dotenv-backed secrets into
${BBLACK}hectic.secret${NC}.
${BGREEN}Options:${NC}
${BCYAN}-h${NC}, ${BCYAN}--help${NC} Show this help message
${BCYAN}-u${NC}, ${BCYAN}--url${NC} <url> PostgreSQL connection string
${BCYAN}-f${NC}, ${BCYAN}--dotenv-file${NC} <path> Dotenv file path
${BGREEN}Resolution order:${NC}
1. ${BBLACK}HECTIC_DOTENV_CONTENT${NC}
2. ${BBLACK}--dotenv-file${NC} / ${BBLACK}HECTIC_DOTENV_FILE${NC}
3. ${BBLACK}\$LOCAL_DIR/.env.\$ENVIRONMENT${NC}
Fails with exit code ${BBLACK}3${NC} when neither a PostgreSQL URL nor a dotenv
source can be resolved.
EOF
)"
}
resolve_pgurl() {
if [ -n "${PGURL:-}" ]; then
printf '%s' "$PGURL"
elif [ -n "${DB_URL:-}" ]; then
printf '%s' "$DB_URL"
else
return 1
fi
}
resolve_dotenv_content() {
dotenv_file="${1:-}"
if [ -n "${HECTIC_DOTENV_CONTENT:-}" ]; then
printf '%s' "$HECTIC_DOTENV_CONTENT"
elif [ -n "$dotenv_file" ]; then
if [ ! -f "$dotenv_file" ] || [ ! -r "$dotenv_file" ]; then
return 2
fi
cat "$dotenv_file"
elif [ -n "${ENVIRONMENT:-}" ] && [ -n "${LOCAL_DIR:-}" ] && [ -r "${LOCAL_DIR}/.env.${ENVIRONMENT}" ]; then
cat "${LOCAL_DIR}/.env.${ENVIRONMENT}"
else
return 1
fi
}
subcommand_secrets_load() {
change_namespace 'db ops secrets load'
dotenv_file="${HECTIC_DOTENV_FILE:-}"
while [ $# -gt 0 ]; do
case $1 in
-h|--help)
help_secrets_load
restore_namespace
exit 0
;;
-u|--url)
if [ $# -lt 2 ]; then
log error "missing value for $1"
restore_namespace
exit 3
fi
PGURL=$2
DB_URL=$2
shift 2
;;
-f|--dotenv-file)
if [ $# -lt 2 ]; then
log error "missing value for $1"
restore_namespace
exit 3
fi
dotenv_file=$2
shift 2
;;
--*|-*)
log error "secrets load argument $1 does not exist"
restore_namespace
exit 9
;;
*)
log error "secrets load subcommand $1 does not exist"
restore_namespace
exit 9
;;
esac
done
if ! pgurl="$(resolve_pgurl)"; then
log error "PGURL or DB_URL is required"
restore_namespace
exit 3
fi
if dotenv_content="$(resolve_dotenv_content "$dotenv_file")"; then
:
else
dotenv_content_exit_code=$?
if [ "$dotenv_content_exit_code" -eq 2 ]; then
log error "dotenv file is not readable: $dotenv_file"
else
log error "dotenv source is required (HECTIC_DOTENV_CONTENT, readable dotenv file, or \$LOCAL_DIR/.env.\$ENVIRONMENT)"
fi
restore_namespace
exit 3
fi
log notice "apply hectic bundle and load secrets"
apply_hectic_bundle "$pgurl" "$dotenv_content"
restore_namespace
}
subcommand_secrets() {
change_namespace 'db ops secrets'
if [ $# -eq 0 ]; then
help_secrets_load
restore_namespace
exit 0
fi
subcommand=$1
shift
case $subcommand in
load)
subcommand_secrets_load "$@"
;;
-h|--help)
help_secrets_load
restore_namespace
exit 0
;;
*)
log error "secrets subcommand $subcommand does not exist"
restore_namespace
exit 9
;;
esac
}
while [ $# -gt 0 ]; do
case $1 in
-h|--help)
help
exit 0
;;
-u|--url)
if [ $# -lt 2 ]; then
log error "missing value for $1"
exit 3
fi
PGURL=$2
DB_URL=$2
shift 2
;;
--*|-*)
log error "argument $1 does not exist"
exit 9
;;
*)
break
;;
esac
done
subcommand="${1:-}"
if [ -z "$subcommand" ]; then
help
exit 0
fi
shift
case $subcommand in
secrets)
subcommand_secrets "$@"
;;
*)
log error "subcommand $subcommand does not exist"
exit 9
;;
esac
+29 -3
View File
@@ -8,7 +8,7 @@ let
applyBundle = self.lib.hectic.applyBundleScript;
mkDatabase =
mkDbDev =
{ postgresql ? postgresql_17 }:
hectic.writeShellApplication {
inherit shell;
@@ -27,7 +27,7 @@ let
${builtins.readFile hectic.helpers.posix-shell.pager_or_cat}
${builtins.readFile hectic.helpers.posix-shell.with_closed_fds}
${applyBundle}
${builtins.readFile ./database.sh}
${builtins.readFile ./db-dev.sh}
'';
meta = {
@@ -36,6 +36,31 @@ let
};
};
mkDbOps =
{ postgresql ? postgresql_17 }:
hectic.writeShellApplication {
inherit shell;
bashOptions = [
"errexit"
"nounset"
];
excludeShellChecks = [ "SC2209" ];
name = "db-ops";
runtimeInputs = [ postgresql coreutils ];
text = ''
${builtins.readFile hectic.helpers.posix-shell.log}
${builtins.readFile hectic.helpers.posix-shell.change_namespace}
${applyBundle}
${builtins.readFile ./db-ops.sh}
'';
meta = {
description = "PostgreSQL operations utility";
mainProgram = "db-ops";
};
};
mkPostgresInit =
{ postgresql ? postgresql_17 }:
hectic.writeShellApplication {
@@ -73,7 +98,8 @@ let
};
in
{
"db-tool" = lib.makeOverridable mkDatabase { };
"db-dev" = lib.makeOverridable mkDbDev { };
"db-ops" = lib.makeOverridable mkDbOps { };
"postgres-init" = lib.makeOverridable mkPostgresInit { };
"postgres-cleanup" = lib.makeOverridable mkPostgresCleanup { };
"hectic-inheritance" = hecticInheritance;
+37
View File
@@ -1,9 +1,46 @@
#!/bin/dash
# Stop a local PostgreSQL cluster started by postgres-init.
#
# Public contract:
# - Accepts PG_WORKING_DIR directly, or derives it from LOCAL_DIR.
# - If no cluster root can be resolved, exits successfully without doing
# anything. This keeps cleanup safe in traps and partial-init flows.
# - If no postmaster.pid exists, treats the cluster as already stopped.
# - NO_TTY=1 redirects pg_ctl output into a temp log file instead of writing to
# the current terminal.
postgres_cleanup_help() {
cat <<'EOF'
Usage: postgres-cleanup
Stop a local PostgreSQL cluster if it is running.
Environment:
PG_WORKING_DIR Cluster root directory
LOCAL_DIR Fallback root used to derive PG_WORKING_DIR
NO_TTY Redirect pg_ctl output to a temp file
Behavior:
- Returns success if the cluster directory cannot be resolved.
- Returns success if postmaster.pid is already absent.
- Ignores pg_ctl stop errors so cleanup remains trap-safe.
EOF
}
postgres_cleanup_main() {
case "${1:-}" in
-h|--help)
postgres_cleanup_help
return 0
;;
esac
if [ -z "${PG_WORKING_DIR:-}" ] && [ -z "${LOCAL_DIR:-}" ]; then return 0; fi
: "${PG_WORKING_DIR:=$LOCAL_DIR/focus/postgresql}"
if [ -f "${PG_WORKING_DIR}/data/postmaster.pid" ]; then
# Cleanup is intentionally forgiving so it can be used in traps after
# partial startup failures without masking the real error.
if [ "${NO_TTY:-0}" = "1" ]; then
_pg_log="$(mktemp /tmp/postgres-cleanup.XXXXXX.log)"
pg_ctl -D "${PG_WORKING_DIR}/data" -m fast -w stop > "$_pg_log" 2>&1 || :
+155 -5
View File
@@ -1,6 +1,58 @@
#!/bin/dash
# Initialize or reuse a local PostgreSQL cluster for development workflows.
#
# Public contract:
# - Requires either PG_WORKING_DIR or LOCAL_DIR.
# - Produces a ready-to-use database and exports:
# POSTGRESQL_HOST, POSTGRESQL_PORT, POSTGRESQL_USER, POSTGRESQL_DATABASE,
# PGURL, and also the variable named by PG_URL_VAR.
# - Reuses an existing cluster only when PG_REUSE is set and PG_VERSION exists.
# - If a reused cluster cannot start, it is reinitialized automatically.
#
# Important knobs:
# - PG_WORKING_DIR: cluster root (defaults to $LOCAL_DIR/focus/postgresql)
# - PG_DATABASE: database to create/ensure (default: testdb)
# - PG_PORT: socket port (default: 5432)
# - PG_URL_VAR: alternate URL variable to export in addition to PGURL
# - PG_CONF_FILE: base postgresql.conf to copy on fresh init
# - PG_SHARED_PRELOAD_LIBRARIES: preload list; empty string disables pg_cron
# - PG_DISABLE_LOGGING=1: disable logging_collector in generated config
# - NO_TTY=1: redirect pg_ctl stop/start output into temp log files
postgres_init_help() {
cat <<'EOF'
Usage: postgres-init
Initialize or reuse a local PostgreSQL cluster.
Environment:
PG_WORKING_DIR Cluster root directory
LOCAL_DIR Fallback root used to derive PG_WORKING_DIR
PG_DATABASE Database name to create/ensure (default: testdb)
PG_PORT PostgreSQL port / unix socket port (default: 5432)
PG_URL_VAR Extra URL variable to export alongside PGURL
PG_CONF_FILE Base postgresql.conf copied on fresh init only
PG_SHARED_PRELOAD_LIBRARIES Preload list; empty disables pg_cron preload
PG_DISABLE_LOGGING Set to 1 to disable logging_collector
PG_REUSE Reuse existing cluster if PG_VERSION exists
NO_TTY Redirect pg_ctl output to temp files
Behavior:
- Rewrites runtime socket/port/cron settings on every launch.
- Creates the target database if missing.
- If a reused cluster exists but cannot start, reinitializes it automatically.
EOF
}
postgres_init_main() {
case "${1:-}" in
-h|--help)
postgres_init_help
return 0
;;
esac
if [ -z "${PG_WORKING_DIR:-}" ] && [ -z "${LOCAL_DIR:-}" ]; then
printf '%s\n' 'postgres-init: PG_WORKING_DIR or LOCAL_DIR is required' >&2
return 1
@@ -25,20 +77,41 @@ postgres_init_main() {
fi
mkdir -p "$sockdir" || return 1
# Reuse is optimistic: if a cluster exists on disk, try to start it first.
# We only destroy state after a real startup failure proves the cluster is
# broken, which keeps long-lived local DBs intact when possible.
if [ "${PG_REUSE+x}" ] && [ -f "$data/PG_VERSION" ]; then PG_REUSE=1; else PG_REUSE=0; fi
if [ "$PG_REUSE" -eq 0 ]; then
rm -rf "$data" "$sockdir" || return 1
mkdir -p "$sockdir" || return 1
initdb -D "$data" --no-locale -E UTF8 || return 1
if [ -n "${PG_CONF_FILE:-}" ]; then
# PG_CONF_FILE replaces the base postgresql.conf on fresh init only. We
# still append runtime values later so the helper can relocate sockets,
# ports, and cron settings regardless of the custom file contents.
[ -r "$PG_CONF_FILE" ] || { printf '%s\n' "postgres-init: PG_CONF_FILE not readable: $PG_CONF_FILE" >&2; return 1; }
cp -f -- "$PG_CONF_FILE" "$data/postgresql.conf" || return 1
else
{ printf '%s\n' "listen_addresses = ''"; [ "$PG_DISABLE_LOGGING" -eq 0 ] && { printf '%s\n' 'logging_collector = on'; printf '%s\n' "log_directory = 'log'"; }; [ -n "$PG_SHARED_PRELOAD_LIBRARIES" ] && { printf '%s\n' "shared_preload_libraries = '$PG_SHARED_PRELOAD_LIBRARIES'"; printf '%s\n' "cron.database_name = '$db'"; printf '%s\n' "cron.host = '$sockdir'"; }; :; } >> "$data/postgresql.conf" || return 1
{
printf '%s\n' "listen_addresses = ''"
if [ "$PG_DISABLE_LOGGING" -eq 0 ]; then
printf '%s\n' 'logging_collector = on'
printf '%s\n' "log_directory = 'log'"
fi
if [ -n "$PG_SHARED_PRELOAD_LIBRARIES" ]; then
printf '%s\n' "shared_preload_libraries = '$PG_SHARED_PRELOAD_LIBRARIES'"
printf '%s\n' "cron.database_name = '$db'"
printf '%s\n' "cron.host = '$sockdir'"
fi
} >> "$data/postgresql.conf" || return 1
fi
sed -i "1ilocal all all trust" "$data/pg_hba.conf" || return 1
fi
# Rewrite the runtime knobs on every launch. Reused clusters may have been
# started from another workspace/session, so we must override stale socket,
# port, and pg_cron wiring before attempting startup.
[ -f "$data/postgresql.auto.conf" ] || : > "$data/postgresql.auto.conf"
[ -f "$data/pg_ident.conf" ] || : > "$data/pg_ident.conf"
@@ -68,12 +141,85 @@ postgres_init_main() {
printf '%s\n' "cron.host = '$sockdir'"
fi
} >> "$data/postgresql.auto.conf" || return 1
_pg_start_exit=0
if [ "${NO_TTY:-0}" = "1" ]; then
_pg_log="$(mktemp /tmp/postgres-init-start.XXXXXX.log)"
with_closed_fds pg_ctl -D "$data" -o "-F" -w start >"$_pg_log" 2>&1 || return 2
with_closed_fds pg_ctl -D "$data" -o "-F" -w start >"$_pg_log" 2>&1 || _pg_start_exit=$?
printf '%s\n' "postgres-init: pg_ctl start output redirected to $_pg_log" >&2
else
with_closed_fds pg_ctl -D "$data" -o "-F" -w start || return 2
with_closed_fds pg_ctl -D "$data" -o "-F" -w start || _pg_start_exit=$?
fi
if [ "$_pg_start_exit" -ne 0 ]; then
if [ "$PG_REUSE" -eq 1 ]; then
# Self-heal path: the on-disk cluster exists but cannot complete startup
# (for example bad WAL / invalid checkpoint). At this point preserving
# the broken state is less useful than reinitializing automatically.
printf '%s\n' "postgres-init: reused cluster failed to start; reinitializing $wd" >&2
PG_REUSE=0
rm -rf "$data" "$sockdir" || return 1
mkdir -p "$sockdir" || return 1
initdb -D "$data" --no-locale -E UTF8 || return 1
if [ -n "${PG_CONF_FILE:-}" ]; then
[ -r "$PG_CONF_FILE" ] || { printf '%s\n' "postgres-init: PG_CONF_FILE not readable: $PG_CONF_FILE" >&2; return 1; }
cp -f -- "$PG_CONF_FILE" "$data/postgresql.conf" || return 1
else
{
printf '%s\n' "listen_addresses = ''"
if [ "$PG_DISABLE_LOGGING" -eq 0 ]; then
printf '%s\n' 'logging_collector = on'
printf '%s\n' "log_directory = 'log'"
fi
if [ -n "$PG_SHARED_PRELOAD_LIBRARIES" ]; then
printf '%s\n' "shared_preload_libraries = '$PG_SHARED_PRELOAD_LIBRARIES'"
printf '%s\n' "cron.database_name = '$db'"
printf '%s\n' "cron.host = '$sockdir'"
fi
} >> "$data/postgresql.conf" || return 1
fi
sed -i "1ilocal all all trust" "$data/pg_hba.conf" || return 1
[ -f "$data/postgresql.auto.conf" ] || : > "$data/postgresql.auto.conf"
[ -f "$data/pg_ident.conf" ] || : > "$data/pg_ident.conf"
sed -i '/^[[:space:]]*port[[:space:]]*=/d' "$data/postgresql.conf" || return 1
sed -i '/^[[:space:]]*unix_socket_directories[[:space:]]*=/d' "$data/postgresql.conf" || return 1
sed -i '/^[[:space:]]*port[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*unix_socket_directories[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*hba_file[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*ident_file[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*shared_preload_libraries[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*cron\.database_name[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*cron\.host[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
{ printf '%s\n' "port = $PG_PORT"; printf '%s\n' "unix_socket_directories = '$sockdir'"; } >> "$data/postgresql.conf" || return 1
{
printf '%s\n' "port = '$PG_PORT'"
printf '%s\n' "unix_socket_directories = '$sockdir'"
printf '%s\n' "hba_file = '$data/pg_hba.conf'"
printf '%s\n' "ident_file = '$data/pg_ident.conf'"
if [ "$PG_DISABLE_LOGGING" -eq 0 ]; then
printf '%s\n' "logging_collector = 'on'"
else
printf '%s\n' "logging_collector = 'off'"
fi
if [ -n "$PG_SHARED_PRELOAD_LIBRARIES" ]; then
printf '%s\n' "shared_preload_libraries = '$PG_SHARED_PRELOAD_LIBRARIES'"
printf '%s\n' "cron.database_name = '$db'"
printf '%s\n' "cron.host = '$sockdir'"
fi
} >> "$data/postgresql.auto.conf" || return 1
_pg_start_exit=0
if [ "${NO_TTY:-0}" = "1" ]; then
_pg_log="$(mktemp /tmp/postgres-init-start.XXXXXX.log)"
with_closed_fds pg_ctl -D "$data" -o "-F" -w start >"$_pg_log" 2>&1 || _pg_start_exit=$?
printf '%s\n' "postgres-init: pg_ctl start output redirected to $_pg_log" >&2
else
with_closed_fds pg_ctl -D "$data" -o "-F" -w start || _pg_start_exit=$?
fi
[ "$_pg_start_exit" -eq 0 ] || return 2
printf '%s\n' "postgres-init: reinitialized broken cluster at $wd" >&2
else
return 2
fi
fi
user="$(id -un)" || return 1
@@ -87,9 +233,13 @@ postgres_init_main() {
psql -h "$sockdir" -p "$PG_PORT" -d "$db" -v ON_ERROR_STOP=1 -c 'select 1;' || return 1
export POSTGRESQL_HOST="$sockdir" POSTGRESQL_PORT="$PG_PORT" POSTGRESQL_USER="$user" POSTGRESQL_DATABASE="$db"
_pg_url="postgresql://${POSTGRESQL_USER}@/${POSTGRESQL_DATABASE}?host=${POSTGRESQL_HOST}&port=${POSTGRESQL_PORT}"
# Export both names for compatibility: some callers consume plain PGURL,
# while multi-cluster shells also need a project-specific variable like
# WIPE_PGURL or BMSEARCH_PGURL in the same environment.
PGURL="postgresql://${POSTGRESQL_USER}@/${POSTGRESQL_DATABASE}?host=${POSTGRESQL_HOST}&port=${POSTGRESQL_PORT}"
export PGURL
case $PG_URL_VAR in ''|*[!ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_]* ) printf '%s\n' 'postgres-init: invalid PG_URL_VAR' >&2; return 1 ;; esac
export "${PG_URL_VAR}=${_pg_url}" || return 1
export "${PG_URL_VAR}=${PGURL}" || return 1
return 0
}
+5 -1
View File
@@ -132,6 +132,7 @@ in {
github-gh-tl = pkgs.callPackage ./github/gh-tl.nix {};
supabase-with-env-collection = pkgs.callPackage ./supabase-with-env-collection.nix {};
migration-name = pkgs.callPackage ./migration-name.nix {};
plgo = pkgs.callPackage ./plgo {};
pg-from = pkgs.callPackage ./postgres/pg-from/default.nix rust.commonArgs;
pg-schema = pkgs.callPackage ./postgres/pg-schema/default.nix rust.commonArgs;
pg_wdumpall = pkgs.callPackage ./postgres/pg_wdumpall.nix rust.commonArgs;
@@ -148,10 +149,12 @@ in {
deploy = pkgs.callPackage ./deploy { inherit inputs; };
element-web = pkgs.callPackage ./element-web {};
shellplot = pkgs.callPackage ./shellplot {};
which-country-rs = pkgs.callPackage ./which-country-rs {};
onlinepubs2man = pkgs.callPackage ./onlinepubs2man {};
migrator = pkgs.callPackage ./migrator { inherit self; };
"parse-uri" = pkgs.callPackage ./parse-uri {};
"db-tool" = dbToolPkgs."db-tool";
"db-dev" = dbToolPkgs."db-dev";
"db-ops" = dbToolPkgs."db-ops";
"postgres-init" = dbToolPkgs."postgres-init";
"postgres-cleanup" = dbToolPkgs."postgres-cleanup";
"hectic-inheritance" = dbToolPkgs."hectic-inheritance";
@@ -173,5 +176,6 @@ in {
pg-15-ext-smtp-client = buildSmtpExt pkgs "15";
pg-15-ext-plhaskell = buildPlHaskellExt pkgs "15";
pg-15-ext-plsh = buildPlShExt pkgs "15";
stable-video-diffusion-api = pkgs.callPackage ./stable-video-diffusion-api {};
media-browser = pkgs.callPackage ./media-browser {};
}
+1 -1
View File
@@ -36,7 +36,7 @@ let
hash = "sha256-pbzuPgKJ0DmrDSTO7ZTDArX+Xr9k/ndAGZvQg2kMTMQ=";
};
inherit patches;
#inherit patches; #WIP
pnpmDeps = fetchPnpmDeps {
pname = "element";
+2 -2
View File
@@ -1,4 +1,4 @@
{ dash, hectic, sqlite, postgresql_17, gawk, self }:
{ dash, hectic, sqlite, postgresql_17, gawk, coreutils, self }:
let
shell = "${dash}/bin/dash";
bashOptions = [
@@ -11,7 +11,7 @@ let
migrator = hectic.writeShellApplication {
inherit shell bashOptions;
name = "migrator";
runtimeInputs = [ sqlite postgresql_17 gawk ];
runtimeInputs = [ sqlite postgresql_17 gawk coreutils ];
text = ''
${builtins.readFile hectic.helpers.posix-shell.log}
+31 -13
View File
@@ -154,11 +154,11 @@ init() {
shift 2
;;
--*|-*)
printf 'init argument %s does not exists' "$1"
log error "init argument ${WHITE}$1${NC} does not exists"
exit 9
;;
*)
printf 'init command %s does not exists' "$1"
log error "init subcommand ${WHITE}$1${NC} does not exists"
exit 9
;;
esac
@@ -434,6 +434,8 @@ migrate_down() {
target_migration=$(printf '%s' "$fs_migrations" | sed -n "${target_line}p")
printf '%s' "$target_migration"
fi
return 0
}
migrate_up() {
@@ -488,6 +490,7 @@ migrate_up() {
fi
printf '%s' "$target_migration"
return 0
}
migrate_to() {
@@ -518,6 +521,8 @@ migrate_to() {
printf '%s' "$migration_name"
;;
esac
return 0
}
migration_list() {
@@ -566,7 +571,10 @@ index_of() {
# no subshell, no pipeline
while IFS= read -r m; do
[ "$m" = "$name" ] && { printf '%s\n' "$i"; return 0; }
if [ "$m" = "$name" ]; then
printf '%s\n' "$i"
return 0
fi
i=$((i+1))
done <<EOF
$list
@@ -633,7 +641,8 @@ migrate() {
db_mig_count=$(printf '%s' "$db_migrations" | wc -l)
log debug "mig count: $db_mig_count"
# Log migration lists for debugging
# These counts are useful even at normal verbosity because they summarize the
# migration state before any validation or apply/revert work begins.
fs_mig_count=$(printf '%s' "$fs_migrations" | wc -l)
log info "Filesystem migrations found: ${WHITE}$fs_mig_count"
log info "Database migrations applied: ${WHITE}$db_mig_count"
@@ -696,26 +705,35 @@ migrate() {
log info "Migration history validation: ${GREEN}OK${NC} (${WHITE}$i${NC} migrations match)"
eval "set -- $MIGRATOR_REMAINING_ARS"
target_migration="$("migrate_$MIGRATE_SUBCOMMAND" "$@")"
if ! target_migration="$("migrate_$MIGRATE_SUBCOMMAND" "$@")"; then
log error "failed to resolve migration target for ${WHITE}migrate $MIGRATE_SUBCOMMAND${NC}"
exit 1
fi
if [ -z "$db_migrations" ]; then
log info "it'll firs migration"
log info "starting from clean migration state"
current_idx=0
else
current_migration=$(printf '%s\n' "$db_migrations" | tail -n1)
current_idx=$(index_of "$fs_migrations" "$current_migration")
if ! current_idx=$(index_of "$fs_migrations" "$current_migration"); then
log error "current database migration is missing from filesystem: ${WHITE}$current_migration${NC}"
exit 2
fi
fi
log debug "[$WHITE$fs_migrations$NC]"
log debug "$target_migration"
log debug "filesystem migrations: ${WHITE}$fs_migrations${NC}"
log debug "requested target migration: ${WHITE}${target_migration:-<clean state>}${NC}"
if [ -z "$target_migration" ]; then
target_idx=0
else
target_idx=$(index_of "$fs_migrations" "$target_migration")
if ! target_idx=$(index_of "$fs_migrations" "$target_migration"); then
log error "target migration is missing from filesystem: ${WHITE}$target_migration${NC}"
exit 2
fi
fi
log debug "indexes $WHITE$current_idx$NC $WHITE${target_idx}"
log debug "migration indexes: current=${WHITE}$current_idx${NC} target=${WHITE}$target_idx${NC}"
if [ "$target_idx" -eq "$current_idx" ]; then
if [ "$target_idx" -eq 0 ]; then
@@ -903,11 +921,11 @@ list() {
shift
;;
--*|-*)
log error "init argument $1 does not exists"
log error "list argument ${WHITE}$1${NC} does not exists"
exit 9
;;
*)
log error "init subcommand $1 does not exists"
log error "list subcommand ${WHITE}$1${NC} does not exists"
exit 9
;;
esac
+31
View File
@@ -0,0 +1,31 @@
{
buildGoModule,
fetchFromGitHub,
lib,
}:
buildGoModule {
pname = "plgo";
version = "0.2.1-unstable-2026-04-04";
src = fetchFromGitHub {
owner = "mgr43";
repo = "plgo";
rev = "492abd81f50f5f588794bd3f9fa103d14546b275";
hash = "sha256-Ifd2IFA62VtkqCUP0/cD3jT4SqkjqwCpHVolslQbwkA=";
};
vendorHash = "sha256-KIVHepJRfVa0bkTF1A4g6ZFGxDy8OPzY3iwV3MhRyVA=";
proxyVendor = true;
subPackages = [ "cmd/plgo" ];
doCheck = false;
meta = {
description = "Write PostgreSQL extensions in Go";
homepage = "https://github.com/mgr43/plgo";
license = lib.licenses.bsd3;
mainProgram = "plgo";
};
}
+230
View File
@@ -0,0 +1,230 @@
#!/usr/bin/env python3
"""Local HTTP API for Diffusers Stable Video Diffusion image-to-video."""
import os
import threading
import uuid
from importlib import import_module
from pathlib import Path
from typing import Any
torch = import_module("torch")
uvicorn = import_module("uvicorn")
diffusers = import_module("diffusers")
diffusers_utils = import_module("diffusers.utils")
fastapi = import_module("fastapi")
pil_image = import_module("PIL.Image")
pil_unidentified_image_error = import_module("PIL").UnidentifiedImageError
FastAPI = fastapi.FastAPI
File = fastapi.File
Form = fastapi.Form
HTTPException = fastapi.HTTPException
Request = fastapi.Request
UploadFile = fastapi.UploadFile
StableVideoDiffusionPipeline = diffusers.StableVideoDiffusionPipeline
export_to_video = diffusers_utils.export_to_video
HOST = os.environ.get("SVD_API_HOST", "127.0.0.1")
PORT = int(os.environ.get("SVD_API_PORT", "8000"))
MODEL_ID = os.environ.get("SVD_MODEL_ID", "stabilityai/stable-video-diffusion-img2vid-xt")
CACHE_DIR = os.environ.get("SVD_CACHE_DIR") or os.environ.get("HF_HOME")
OUTPUT_DIR = Path(os.environ.get("SVD_OUTPUT_DIR", "/var/lib/stable-video-diffusion-api/outputs"))
DEVICE = os.environ.get("SVD_DEVICE", "cuda" if torch.cuda.is_available() else "cpu")
DTYPE = os.environ.get("SVD_DTYPE", "float16")
ENABLE_CPU_OFFLOAD = os.environ.get("SVD_ENABLE_CPU_OFFLOAD", "0").lower() in ("1", "true", "yes", "on")
DEFAULT_DECODE_CHUNK_SIZE = int(os.environ.get("SVD_DECODE_CHUNK_SIZE", "8"))
DEFAULT_FPS = int(os.environ.get("SVD_FPS", "7"))
app = FastAPI(title="Stable Video Diffusion API")
pipeline_lock = threading.Lock()
pipeline: Any | None = None
def torch_dtype() -> Any:
dtypes = {
"float16": torch.float16,
"fp16": torch.float16,
"float32": torch.float32,
"fp32": torch.float32,
"bfloat16": torch.bfloat16,
"bf16": torch.bfloat16,
}
try:
return dtypes[DTYPE.lower()]
except KeyError as exc:
raise RuntimeError(f"Unsupported SVD_DTYPE: {DTYPE}") from exc
def get_pipeline() -> Any:
global pipeline
if pipeline is not None:
return pipeline
with pipeline_lock:
if pipeline is not None:
return pipeline
kwargs: dict[str, Any] = {"torch_dtype": torch_dtype()}
if CACHE_DIR:
kwargs["cache_dir"] = CACHE_DIR
loaded = StableVideoDiffusionPipeline.from_pretrained(MODEL_ID, **kwargs)
if ENABLE_CPU_OFFLOAD:
loaded.enable_model_cpu_offload()
else:
loaded.to(DEVICE)
pipeline = loaded
return loaded
def positive_int(name: str, value: Any, default: int) -> int:
if value in (None, ""):
return default
try:
parsed = int(value)
except (TypeError, ValueError) as exc:
raise HTTPException(status_code=400, detail=f"{name} must be an integer") from exc
if parsed <= 0:
raise HTTPException(status_code=400, detail=f"{name} must be greater than zero")
return parsed
def optional_int(name: str, value: Any) -> int | None:
if value in (None, ""):
return None
try:
return int(value)
except (TypeError, ValueError) as exc:
raise HTTPException(status_code=400, detail=f"{name} must be an integer") from exc
def optional_float(name: str, value: Any) -> float | None:
if value in (None, ""):
return None
try:
return float(value)
except (TypeError, ValueError) as exc:
raise HTTPException(status_code=400, detail=f"{name} must be a number") from exc
def load_image_from_path(input_image_path: str) -> Any:
path = Path(input_image_path).expanduser()
if not path.exists() or not path.is_file():
raise HTTPException(status_code=400, detail="input_image_path does not exist or is not a file")
try:
return pil_image.open(path).convert("RGB")
except (OSError, pil_unidentified_image_error) as exc:
raise HTTPException(status_code=400, detail="input_image_path could not be decoded as an image") from exc
async def load_uploaded_image(image: UploadFile) -> Any:
try:
return pil_image.open(image.file).convert("RGB")
except (OSError, pil_unidentified_image_error) as exc:
raise HTTPException(status_code=400, detail="uploaded image could not be decoded") from exc
async def json_payload(request: Request) -> dict[str, Any]:
content_type = request.headers.get("content-type", "")
if not content_type.startswith("application/json"):
return {}
try:
payload = await request.json()
except ValueError as exc:
raise HTTPException(status_code=400, detail="request body must be valid JSON") from exc
if not isinstance(payload, dict):
raise HTTPException(status_code=400, detail="JSON request body must be an object")
return payload
@app.get("/health")
def health() -> dict[str, Any]:
return {
"status": "ok",
"model_loaded": pipeline is not None,
"model_id": MODEL_ID,
"device": DEVICE,
"dtype": DTYPE,
"cpu_offload": ENABLE_CPU_OFFLOAD,
"cache_dir": CACHE_DIR,
"output_dir": str(OUTPUT_DIR),
}
@app.post("/generate")
async def generate(
request: Request,
image: UploadFile | None = File(default=None),
input_image_path: str | None = Form(default=None),
num_frames: int | None = Form(default=None),
num_inference_steps: int | None = Form(default=None),
fps: int | None = Form(default=None),
decode_chunk_size: int | None = Form(default=None),
seed: int | None = Form(default=None),
motion_bucket_id: int | None = Form(default=None),
noise_aug_strength: float | None = Form(default=None),
) -> dict[str, Any]:
payload = await json_payload(request)
path = input_image_path or payload.get("input_image_path")
upload = image
if upload is None and not path:
raise HTTPException(status_code=400, detail="provide input_image_path or upload image")
if upload is not None and path:
raise HTTPException(status_code=400, detail="provide only one image source")
source_image = await load_uploaded_image(upload) if upload is not None else load_image_from_path(str(path))
chunk_size = positive_int("decode_chunk_size", decode_chunk_size if decode_chunk_size is not None else payload.get("decode_chunk_size"), DEFAULT_DECODE_CHUNK_SIZE)
output_fps = positive_int("fps", fps if fps is not None else payload.get("fps"), DEFAULT_FPS)
request_seed = optional_int("seed", seed if seed is not None else payload.get("seed"))
generator = None
if request_seed is not None:
generator = torch.Generator(device="cpu").manual_seed(request_seed)
call_args: dict[str, Any] = {
"image": source_image,
"decode_chunk_size": chunk_size,
}
for key, value in {
"num_frames": num_frames if num_frames is not None else payload.get("num_frames"),
"num_inference_steps": num_inference_steps if num_inference_steps is not None else payload.get("num_inference_steps"),
"motion_bucket_id": motion_bucket_id if motion_bucket_id is not None else payload.get("motion_bucket_id"),
}.items():
parsed = optional_int(key, value)
if parsed is not None:
call_args[key] = parsed
parsed_noise = optional_float("noise_aug_strength", noise_aug_strength if noise_aug_strength is not None else payload.get("noise_aug_strength"))
if parsed_noise is not None:
call_args["noise_aug_strength"] = parsed_noise
if generator is not None:
call_args["generator"] = generator
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
output_path = OUTPUT_DIR / f"svd-{uuid.uuid4().hex}.mp4"
try:
frames = get_pipeline()(**call_args).frames[0]
export_to_video(frames, str(output_path), fps=output_fps)
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
return {
"output_path": str(output_path),
"model_id": MODEL_ID,
"seed": request_seed,
"fps": output_fps,
}
def main() -> None:
uvicorn.run("app:app", host=HOST, port=PORT)
if __name__ == "__main__":
main()
@@ -0,0 +1,41 @@
{ pkgs }:
let
pythonEnv = pkgs.python3.withPackages (ps: let
torch = ps.torchWithCuda;
accelerate = ps.accelerate.override { inherit torch; };
in [
accelerate
ps.diffusers
ps.fastapi
ps.imageio
ps.imageio-ffmpeg
ps.pillow
ps.python-multipart
ps.safetensors
torch
ps.transformers
ps.uvicorn
]);
in
pkgs.stdenv.mkDerivation {
pname = "stable-video-diffusion-api";
version = "0.1.0";
src = ./.;
nativeBuildInputs = [ pkgs.makeWrapper ];
installPhase = ''
mkdir -p $out/bin $out/libexec/stable-video-diffusion-api
cp $src/app.py $out/libexec/stable-video-diffusion-api/app.py
chmod +x $out/libexec/stable-video-diffusion-api/app.py
makeWrapper ${pythonEnv}/bin/python3 $out/bin/stable-video-diffusion-api \
--add-flags $out/libexec/stable-video-diffusion-api/app.py \
--set-default SVD_API_HOST 127.0.0.1 \
--set-default SVD_API_PORT 8000 \
--set-default SVD_MODEL_ID stabilityai/stable-video-diffusion-img2vid-xt \
--set-default SVD_OUTPUT_DIR /var/lib/stable-video-diffusion-api/outputs
'';
}
+1939
View File
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "which-country-rs"
version = "1.0.0"
edition = "2024"
description = "CLI tool that detects your country by IP and renders an ASCII map"
license = "MIT"
repository = "https://github.com/krisfur/which-country-rs"
keywords = ["cli", "geolocation", "ascii", "map"]
categories = ["command-line-utilities"]
[features]
default = ["geoip"]
geoip = ["reqwest"]
[dependencies]
clap = { version = "4", features = ["derive"] }
reqwest = { version = "0.13", features = ["blocking", "json"], optional = true }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Krzysztof Furman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+83
View File
@@ -0,0 +1,83 @@
# which-country-rs
A CLI tool that detects your country from your IP address and renders an ASCII map zoomed into it, showing neighbouring borders and country codes.
## Example output
```bash
which-country-rs -c DE
```
```
You appear to be in: Germany (DE)
·········· ·· ······ · ·
· NO · ·· · ···
· · ···· ··· ···········
· ····· ·· ············
····· ········ · ·· ···EE ·
· ······ ······ ·· · LV····· ·····
·· ··· ·DK ·· ··· ···· ··········· ···
···· ··· ··· · · ··· ···· ··· LT ···· ···
· ····· ···· ··· ############ ··················· ···
IE ·· ····GB · ·····##### ## ## ···· BY
· ·· ·· ··· ·NL # ## ··
··· ····· ·· ····· ## # PL ·············
····················BE ··# DE #######····· ····
· ······· ···L## ### CZ ····· ······
······· #### ###·········SK ······· ··
····· ·###### ###### AT ········ ···············
·· FR ··· CH ··#·············· HU ·· MD··
· · ····· · ····SI··· ········· RO ····
· · · ···HR········ RS···· ···
········ ·· · ········ IT ··· ·····BA ·· ··············
· ·· ········· ·········· ·· · · ···ME··XK···· ··
····· ··· · ·· · ··· ···· ·· ··MK·· ·BG ·····
· ·· ····· ··· ····· ···· ·AL············ ······
· ·· ES · · · ·· ····· ···· ···· ······
Coordinates: 51.15°N, 10.55°E
```
## Usage
```
# Auto-detect from IP
which-country-rs
# Specify a country code
which-country-rs -c JP
# Specify coordinates (supports negative values for south/west)
which-country-rs --lat 40 --lon -74
# Custom map size
which-country-rs -W 120 -H 40
```
### Options
| Flag | Description | Default |
|------|-------------|---------|
| `-W, --width` | Map width in characters | 80 |
| `-H, --height` | Map height in characters | 24 |
| `-c, --country` | ISO 3166-1 alpha-2 country code (skips IP lookup) | |
| `--lat` | Latitude (requires `--lon`) | |
| `--lon` | Longitude (requires `--lat`) | |
| `-V, --version` | Print version | |
## Building
```
cargo build --release
```
To build without IP geolocation support (drops the `reqwest` dependency):
```
cargo build --release --no-default-features
```
## Map data
Country borders from [Natural Earth](https://www.naturalearthdata.com/) 110m admin-0 countries (public domain).
File diff suppressed because one or more lines are too long
+25
View File
@@ -0,0 +1,25 @@
{ lib, pkgs, ... }: let
src = ./.;
in
pkgs.rustPlatform.buildRustPackage {
pname = "which-country-rs";
version = "1.0.0";
inherit src;
cargoLock.lockFile = ./Cargo.lock;
# NOTE(yukkop): upstream uses edition 2024, so do not pass the repo's
# Rust 1.81 commonArgs here.
# NOTE(yukkop): keep the Nix package offline-by-default; upstream's
# default geoip feature calls a plaintext HTTP endpoint.
cargoBuildFlags = ["--no-default-features"];
cargoTestFlags = ["--no-default-features"];
meta = {
description = "CLI tool that tells you which country contains coordinates";
homepage = "https://github.com/krisfur/which-country-rs";
license = lib.licenses.mit;
mainProgram = "which-country-rs";
};
}
+275
View File
@@ -0,0 +1,275 @@
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct FeatureCollection {
features: Vec<Feature>,
}
#[derive(Debug, Deserialize)]
struct Feature {
properties: Properties,
geometry: Geometry,
}
#[derive(Debug, Deserialize)]
struct Properties {
#[serde(rename = "ISO_A2_EH")]
iso_a2: String,
#[serde(rename = "NAME")]
name: String,
}
#[derive(Debug, Deserialize)]
#[serde(tag = "type")]
enum Geometry {
Polygon {
coordinates: Vec<Vec<[f64; 2]>>,
},
MultiPolygon {
coordinates: Vec<Vec<Vec<[f64; 2]>>>,
},
}
#[allow(dead_code)]
pub struct Country {
pub iso_a2: String,
pub name: String,
/// Each polygon is a list of rings; ring 0 = outer, rest = holes
pub polygons: Vec<Vec<Vec<[f64; 2]>>>,
pub bbox: (f64, f64, f64, f64), // (min_lon, min_lat, max_lon, max_lat)
pub label_pos: (f64, f64), // (lon, lat) — centroid of largest polygon
}
/// Signed area of a ring (positive = CCW).
fn ring_signed_area(ring: &[[f64; 2]]) -> f64 {
let n = ring.len();
if n < 3 {
return 0.0;
}
let mut area = 0.0;
let mut j = n - 1;
for i in 0..n {
area += (ring[j][0] - ring[i][0]) * (ring[j][1] + ring[i][1]);
j = i;
}
area / 2.0
}
/// Ray-casting point-in-ring test.
fn point_in_ring(lon: f64, lat: f64, ring: &[[f64; 2]]) -> bool {
let mut inside = false;
let n = ring.len();
if n < 3 {
return false;
}
let mut j = n - 1;
for i in 0..n {
let xi = ring[i][0];
let yi = ring[i][1];
let xj = ring[j][0];
let yj = ring[j][1];
if ((yi > lat) != (yj > lat)) && (lon < (xj - xi) * (lat - yi) / (yj - yi) + xi) {
inside = !inside;
}
j = i;
}
inside
}
/// Find horizontal interior spans at a given latitude.
/// Returns sorted pairs of (enter_lon, exit_lon).
fn horizontal_spans(lat: f64, ring: &[[f64; 2]]) -> Vec<(f64, f64)> {
let n = ring.len();
if n < 3 {
return Vec::new();
}
let mut crossings = Vec::new();
let mut j = n - 1;
for i in 0..n {
let yi = ring[i][1];
let yj = ring[j][1];
if (yi > lat) != (yj > lat) {
let xi = ring[i][0];
let xj = ring[j][0];
crossings.push((xj - xi) * (lat - yi) / (yj - yi) + xi);
}
j = i;
}
crossings.sort_by(|a, b| a.partial_cmp(b).unwrap());
crossings.chunks_exact(2).map(|p| (p[0], p[1])).collect()
}
/// Find vertical interior spans at a given longitude.
/// Returns sorted pairs of (enter_lat, exit_lat).
fn vertical_spans(lon: f64, ring: &[[f64; 2]]) -> Vec<(f64, f64)> {
let n = ring.len();
if n < 3 {
return Vec::new();
}
let mut crossings = Vec::new();
let mut j = n - 1;
for i in 0..n {
let xi = ring[i][0];
let xj = ring[j][0];
if (xi > lon) != (xj > lon) {
let yi = ring[i][1];
let yj = ring[j][1];
crossings.push((yj - yi) * (lon - xi) / (xj - xi) + yi);
}
j = i;
}
crossings.sort_by(|a, b| a.partial_cmp(b).unwrap());
crossings.chunks_exact(2).map(|p| (p[0], p[1])).collect()
}
/// Find a good interior label point for a ring.
/// Scans a grid of candidate points and picks the one that maximizes
/// min(half_width, half_height) — the "most interior" point.
fn ring_label_point(ring: &[[f64; 2]]) -> (f64, f64) {
let min_lon = ring.iter().map(|c| c[0]).fold(f64::MAX, f64::min);
let max_lon = ring.iter().map(|c| c[0]).fold(f64::MIN, f64::max);
let min_lat = ring.iter().map(|c| c[1]).fold(f64::MAX, f64::min);
let max_lat = ring.iter().map(|c| c[1]).fold(f64::MIN, f64::max);
let steps = 24;
let mut best = ((min_lon + max_lon) / 2.0, (min_lat + max_lat) / 2.0);
let mut best_score = 0.0f64;
for row in 1..steps {
let lat = min_lat + (max_lat - min_lat) * row as f64 / steps as f64;
let h_spans = horizontal_spans(lat, ring);
for &(span_left, span_right) in &h_spans {
let mid_lon = (span_left + span_right) / 2.0;
let half_w = (span_right - span_left) / 2.0;
// Measure vertical extent at this longitude
let v_spans = vertical_spans(mid_lon, ring);
for &(span_bot, span_top) in &v_spans {
if lat >= span_bot && lat <= span_top {
let half_h = ((lat - span_bot).min(span_top - lat)).min(half_w);
let score = half_w.min(half_h);
if score > best_score {
best_score = score;
best = (mid_lon, lat);
}
break;
}
}
}
}
best
}
pub fn load_countries(geojson: &str) -> Vec<Country> {
let fc: FeatureCollection = serde_json::from_str(geojson).expect("Failed to parse GeoJSON");
fc.features
.into_iter()
.map(|f| {
let polygons = match f.geometry {
Geometry::Polygon { coordinates } => vec![coordinates],
Geometry::MultiPolygon { coordinates } => coordinates,
};
let mut min_lon = f64::MAX;
let mut min_lat = f64::MAX;
let mut max_lon = f64::MIN;
let mut max_lat = f64::MIN;
for poly in &polygons {
for ring in poly {
for coord in ring {
let lon = coord[0];
let lat = coord[1];
if lon < min_lon {
min_lon = lon;
}
if lon > max_lon {
max_lon = lon;
}
if lat < min_lat {
min_lat = lat;
}
if lat > max_lat {
max_lat = lat;
}
}
}
}
// Label inside the largest polygon
let label_pos = polygons
.iter()
.filter(|p| !p.is_empty() && p[0].len() >= 3)
.max_by(|a, b| {
ring_signed_area(&a[0])
.abs()
.partial_cmp(&ring_signed_area(&b[0]).abs())
.unwrap()
})
.map(|p| ring_label_point(&p[0]))
.unwrap_or(((min_lon + max_lon) / 2.0, (min_lat + max_lat) / 2.0));
Country {
iso_a2: f.properties.iso_a2,
name: f.properties.name,
polygons,
bbox: (min_lon, min_lat, max_lon, max_lat),
label_pos,
}
})
.collect()
}
/// Check if a point is inside a polygon (outer ring minus holes).
fn point_in_polygon(lon: f64, lat: f64, rings: &[Vec<[f64; 2]>]) -> bool {
if rings.is_empty() || !point_in_ring(lon, lat, &rings[0]) {
return false;
}
// Must be outside all holes
!rings[1..].iter().any(|hole| point_in_ring(lon, lat, hole))
}
/// Check if a point falls inside a country.
pub fn point_in_country(lon: f64, lat: f64, country: &Country) -> bool {
let (min_lon, min_lat, max_lon, max_lat) = country.bbox;
if lon < min_lon || lon > max_lon || lat < min_lat || lat > max_lat {
return false;
}
country
.polygons
.iter()
.any(|poly| point_in_polygon(lon, lat, poly))
}
/// Find which country contains the given point, with a nearest-country
/// fallback for when low-res coastlines cause a near miss.
pub fn find_country(lon: f64, lat: f64, countries: &[Country]) -> Option<usize> {
// Exact hit
if let Some(idx) = countries.iter().position(|c| point_in_country(lon, lat, c)) {
return Some(idx);
}
// Search in expanding rings up to ~1 degree
for &offset in &[0.25, 0.5, 1.0] {
for &(dlon, dlat) in &[
(offset, 0.0),
(-offset, 0.0),
(0.0, offset),
(0.0, -offset),
(offset, offset),
(offset, -offset),
(-offset, offset),
(-offset, -offset),
] {
if let Some(idx) = countries
.iter()
.position(|c| point_in_country(lon + dlon, lat + dlat, c))
{
return Some(idx);
}
}
}
None
}
+44
View File
@@ -0,0 +1,44 @@
use serde::Deserialize;
#[derive(Debug, Deserialize)]
#[serde(tag = "status")]
pub enum GeoIpResponse {
#[serde(rename = "success")]
Success {
country: String,
#[serde(rename = "countryCode")]
country_code: String,
lat: f64,
lon: f64,
},
#[serde(rename = "fail")]
Fail { message: String },
}
pub struct GeoIpResult {
pub country: String,
pub country_code: String,
pub lat: f64,
pub lon: f64,
}
pub fn lookup() -> Result<GeoIpResult, String> {
let url = "http://ip-api.com/json/?fields=status,message,countryCode,country,lat,lon";
let resp = reqwest::blocking::get(url).map_err(|e| format!("HTTP request failed: {e}"))?;
let data: GeoIpResponse = resp.json().map_err(|e| format!("Failed to parse response: {e}"))?;
match data {
GeoIpResponse::Success {
country,
country_code,
lat,
lon,
} => Ok(GeoIpResult {
country,
country_code,
lat,
lon,
}),
GeoIpResponse::Fail { message } => Err(format!("GeoIP lookup failed: {message}")),
}
}
+99
View File
@@ -0,0 +1,99 @@
mod geo;
#[cfg(feature = "geoip")]
mod geoip;
mod render;
use clap::Parser;
#[derive(Parser)]
#[command(name = "which-country-rs", version, about = "Detect your country by IP and render an ASCII world map")]
struct Args {
/// Map width in characters
#[arg(short = 'W', long, default_value_t = 80)]
width: usize,
/// Map height in characters
#[arg(short = 'H', long, default_value_t = 24)]
height: usize,
/// Country code to display (e.g. "US", "FR", "JP") — skips IP lookup
#[arg(short, long)]
country: Option<String>,
/// Latitude (requires --lon too) — skips IP lookup, derives country from coordinates
#[arg(long, requires = "lon", allow_hyphen_values = true)]
lat: Option<f64>,
/// Longitude (requires --lat too)
#[arg(long, requires = "lat", allow_hyphen_values = true)]
lon: Option<f64>,
}
static GEOJSON: &str = include_str!("../data/countries.geojson");
fn main() {
let args = Args::parse();
let countries = geo::load_countries(GEOJSON);
let (country_name, country_code, lat, lon) = if let Some(code) = &args.country {
// Direct country code — find it in the data
let code_upper = code.to_uppercase();
let c = countries
.iter()
.find(|c| c.iso_a2 == code_upper)
.unwrap_or_else(|| {
eprintln!("Unknown country code: {code_upper}");
std::process::exit(1);
});
let (label_lon, label_lat) = c.label_pos;
(c.name.clone(), code_upper, label_lat, label_lon)
} else if let (Some(lat), Some(lon)) = (args.lat, args.lon) {
// Coordinates provided — find which country contains the point
let idx = geo::find_country(lon, lat, &countries)
.unwrap_or_else(|| {
eprintln!("No country found at {lat}, {lon} (ocean?)");
std::process::exit(1);
});
(
countries[idx].name.clone(),
countries[idx].iso_a2.clone(),
lat,
lon,
)
} else {
// IP geolocation
#[cfg(feature = "geoip")]
{
eprint!("Looking up your location... ");
match geoip::lookup() {
Ok(loc) => {
eprintln!("done.");
(loc.country, loc.country_code, loc.lat, loc.lon)
}
Err(e) => {
eprintln!("error: {e}");
std::process::exit(1);
}
}
}
#[cfg(not(feature = "geoip"))]
{
eprintln!("IP geolocation requires the 'geoip' feature. Use --country or --lat/--lon instead.");
std::process::exit(1);
}
};
let map = render::render_map(&countries, &country_code, args.width, args.height);
println!("You appear to be in: {country_name} ({country_code})");
println!();
println!("{map}");
println!();
println!(
"Coordinates: {:.2}\u{00b0}{}, {:.2}\u{00b0}{}",
lat.abs(),
if lat >= 0.0 { "N" } else { "S" },
lon.abs(),
if lon >= 0.0 { "E" } else { "W" },
);
}
+184
View File
@@ -0,0 +1,184 @@
use crate::geo::Country;
/// Render a zoomed-in ASCII map centered on the target country, showing borders and labels.
pub fn render_map(
countries: &[Country],
target_code: &str,
width: usize,
height: usize,
) -> String {
// Find the target country and compute its bbox
let target_idx = countries
.iter()
.position(|c| c.iso_a2 == target_code)
.expect("Target country not found in GeoJSON");
let (min_lon, min_lat, max_lon, max_lat) = countries[target_idx].bbox;
// Add generous padding so the surrounding continent is visible
let lon_span = (max_lon - min_lon).max(4.0);
let lat_span = (max_lat - min_lat).max(4.0);
let pad_lon = lon_span * 1.0;
let pad_lat = lat_span * 1.0;
let view_min_lon = (min_lon - pad_lon).max(-180.0);
let view_max_lon = (max_lon + pad_lon).min(180.0);
let view_min_lat = (min_lat - pad_lat).max(-90.0);
let view_max_lat = (max_lat + pad_lat).min(90.0);
// Adjust aspect ratio: terminal chars are ~2x taller than wide
let lon_range = view_max_lon - view_min_lon;
let lat_range = view_max_lat - view_min_lat;
let char_aspect = 2.0;
let (final_lon_range, final_lat_range);
let desired_lon = lat_range * (width as f64) / (height as f64) / char_aspect;
let desired_lat = lon_range * (height as f64) / (width as f64) * char_aspect;
if desired_lon > lon_range {
final_lon_range = desired_lon;
final_lat_range = lat_range;
} else {
final_lon_range = lon_range;
final_lat_range = desired_lat;
}
let center_lon = (view_min_lon + view_max_lon) / 2.0;
let center_lat = (view_min_lat + view_max_lat) / 2.0;
let vp_min_lon = (center_lon - final_lon_range / 2.0).max(-180.0);
let vp_max_lat = (center_lat + final_lat_range / 2.0).min(90.0);
let lon_per_col = final_lon_range / width as f64;
let lat_per_row = final_lat_range / height as f64;
// Grid of characters
let mut grid = vec![vec![' '; width]; height];
// Rasterize polygon edges onto the grid
for (i, country) in countries.iter().enumerate() {
let (c_min_lon, c_min_lat, c_max_lon, c_max_lat) = country.bbox;
if c_max_lon < vp_min_lon
|| c_min_lon > vp_min_lon + final_lon_range
|| c_max_lat < vp_max_lat - final_lat_range
|| c_min_lat > vp_max_lat
{
continue;
}
let border_ch = if i == target_idx { '#' } else { '\u{00b7}' };
for poly in &country.polygons {
for ring in poly {
if ring.len() < 2 {
continue;
}
for edge in ring.windows(2) {
rasterize_edge(
edge[0][0],
edge[0][1],
edge[1][0],
edge[1][1],
vp_min_lon,
vp_max_lat,
lon_per_col,
lat_per_row,
width,
height,
border_ch,
i == target_idx,
&mut grid,
);
}
}
}
}
// Place country code labels at bbox center
for (i, country) in countries.iter().enumerate() {
if country.iso_a2 == "-99" {
continue;
}
let (label_lon, label_lat) = country.label_pos;
let col = ((label_lon - vp_min_lon) / lon_per_col) as isize;
let row = ((vp_max_lat - label_lat) / lat_per_row) as isize;
let label = &country.iso_a2;
let start_col = col - (label.len() as isize / 2);
for (j, ch) in label.chars().enumerate() {
let c = start_col + j as isize;
if c >= 0 && (c as usize) < width && row >= 0 && (row as usize) < height {
let r = row as usize;
let c = c as usize;
let existing = grid[r][c];
// Target label always writes; neighbor labels only on empty or neighbor border
if i == target_idx || existing == ' ' || existing == '\u{00b7}' {
grid[r][c] = ch;
}
}
}
}
// Render grid to string
grid.iter()
.map(|row| row.iter().collect::<String>())
.collect::<Vec<_>>()
.join("\n")
}
/// Rasterize a line segment onto the grid using Bresenham's algorithm.
fn rasterize_edge(
lon0: f64,
lat0: f64,
lon1: f64,
lat1: f64,
vp_min_lon: f64,
vp_max_lat: f64,
lon_per_col: f64,
lat_per_row: f64,
width: usize,
height: usize,
ch: char,
is_target: bool,
grid: &mut [Vec<char>],
) {
let c0 = ((lon0 - vp_min_lon) / lon_per_col) as i32;
let r0 = ((vp_max_lat - lat0) / lat_per_row) as i32;
let c1 = ((lon1 - vp_min_lon) / lon_per_col) as i32;
let r1 = ((vp_max_lat - lat1) / lat_per_row) as i32;
let mut x = c0;
let mut y = r0;
let dx = (c1 - c0).abs();
let dy = -(r1 - r0).abs();
let sx = if c0 < c1 { 1 } else { -1 };
let sy = if r0 < r1 { 1 } else { -1 };
let mut err = dx + dy;
loop {
if x >= 0 && (x as usize) < width && y >= 0 && (y as usize) < height {
let cell = &mut grid[y as usize][x as usize];
// Target borders overwrite neighbor borders; neighbor borders only on empty
if is_target || *cell == ' ' {
*cell = ch;
}
}
if x == c1 && y == r1 {
break;
}
let e2 = 2 * err;
if e2 >= dy {
err += dy;
x += sx;
}
if e2 <= dx {
err += dx;
y += sy;
}
}
}
+4 -3
View File
@@ -31,7 +31,8 @@
) (lib.filterAttrs (_: v: v != null)
(lib.mapAttrs (n: t: mkTestDrv folder n t) (testDir folder)));
database = self.packages.${system}."db-tool";
database = self.packages.${system}."db-dev";
dbOps = self.packages.${system}."db-ops";
postgresInit = self.packages.${system}."postgres-init";
postgresCleanup = self.packages.${system}."postgres-cleanup";
@@ -48,7 +49,7 @@
mkNonPgTest = testName: testDrv: pkgs.runCommand "db-tool-${testName}"
{
nativeBuildInputs = [ pkgs.coreutils pkgs.gnugrep pkgs.gnused ];
buildInputs = [ database postgresInit postgresCleanup pkgs.postgresql_17 pkgs.dash ];
buildInputs = [ database dbOps postgresInit postgresCleanup pkgs.postgresql_17 pkgs.dash ];
} ''
${builtins.readFile self.legacyPackages.${system}.helpers.posix-shell.log}
test=${testDrv}
@@ -64,7 +65,7 @@
mkPgTest = testName: testDrv: pkgs.runCommand "db-tool-${testName}"
{
nativeBuildInputs = [ pkgs.coreutils pkgs.gnugrep pkgs.gnused ];
buildInputs = [ database postgresInit postgresCleanup pkgs.postgresql_17 pkgs.dash pkgs.netcat-openbsd ];
buildInputs = [ database dbOps postgresInit postgresCleanup pkgs.postgresql_17 pkgs.dash pkgs.netcat-openbsd ];
} ''
${builtins.readFile self.legacyPackages.${system}.helpers.posix-shell.log}
test=${testDrv}
+19
View File
@@ -0,0 +1,19 @@
# shellcheck shell=dash
export HECTIC_NAMESPACE=test-db-ops-help
log notice "test case: db-ops --help exits 0"
if ! db-ops --help > /tmp/db-ops-help-out.txt 2>&1; then
log error "test failed: db-ops --help exited non-zero"
exit 1
fi
for tok in secrets load HECTIC_DOTENV_FILE PGURL DB_URL; do
if ! grep -qF "$tok" /tmp/db-ops-help-out.txt; then
log error "test failed: db-ops --help output missing token: $tok"
cat /tmp/db-ops-help-out.txt >&2
exit 1
fi
done
log notice "test passed"
@@ -0,0 +1,24 @@
# shellcheck shell=dash
export HECTIC_NAMESPACE=test-db-ops-missing-dotenv
if db-ops --url 'postgresql://example.invalid/test' secrets load > /tmp/db-ops-missing-dotenv.out 2>&1; then
log error "expected db-ops secrets load to fail without dotenv source"
exit 1
else
exit_code=$?
fi
[ "$exit_code" -eq 3 ] || {
log error "expected exit code 3 without dotenv source, got: $exit_code"
cat /tmp/db-ops-missing-dotenv.out >&2
exit 1
}
if ! grep -qF 'dotenv source is required' /tmp/db-ops-missing-dotenv.out; then
log error "missing dotenv source error message"
cat /tmp/db-ops-missing-dotenv.out >&2
exit 1
fi
log notice "test passed"
@@ -0,0 +1,28 @@
# shellcheck shell=dash
export HECTIC_NAMESPACE=test-db-ops-missing-pgurl
dotenv_file=$(mktemp)
trap 'rm -f "$dotenv_file"' EXIT INT TERM
printf 'TEST_SECRET=hello-world\n' > "$dotenv_file"
if db-ops secrets load --dotenv-file "$dotenv_file" > /tmp/db-ops-missing-pgurl.out 2>&1; then
log error "expected db-ops secrets load to fail without PGURL"
exit 1
else
exit_code=$?
fi
[ "$exit_code" -eq 3 ] || {
log error "expected exit code 3 without PGURL, got: $exit_code"
cat /tmp/db-ops-missing-pgurl.out >&2
exit 1
}
if ! grep -qF 'PGURL or DB_URL is required' /tmp/db-ops-missing-pgurl.out; then
log error "missing PGURL error message"
cat /tmp/db-ops-missing-pgurl.out >&2
exit 1
fi
log notice "test passed"
@@ -0,0 +1,28 @@
# shellcheck shell=dash
export HECTIC_NAMESPACE=test-db-ops-unreadable-dotenv
dotenv_dir=$(mktemp -d)
dotenv_file="$dotenv_dir/missing.env"
trap 'rm -rf "$dotenv_dir"' EXIT INT TERM
if db-ops --url 'postgresql://example.invalid/test' secrets load --dotenv-file "$dotenv_file" > /tmp/db-ops-unreadable-dotenv.out 2>&1; then
log error "expected db-ops secrets load to fail for unreadable explicit dotenv path"
exit 1
else
exit_code=$?
fi
[ "$exit_code" -eq 3 ] || {
log error "expected exit code 3 for unreadable explicit dotenv path, got: $exit_code"
cat /tmp/db-ops-unreadable-dotenv.out >&2
exit 1
}
if ! grep -qF 'dotenv file is not readable' /tmp/db-ops-unreadable-dotenv.out; then
log error "missing unreadable dotenv file error message"
cat /tmp/db-ops-unreadable-dotenv.out >&2
exit 1
fi
log notice "test passed"
@@ -0,0 +1,56 @@
# shellcheck shell=dash
HECTIC_NAMESPACE=test-db-ops-secrets-load
PG_WORKING_DIR=$(mktemp -d)
export PG_WORKING_DIR PG_DATABASE=testdb PG_PORT=5432 PG_SHARED_PRELOAD_LIBRARIES=''
cleanup() {
postgres-cleanup >/dev/null 2>&1 || :
rm -rf "$PG_WORKING_DIR"
}
trap 'cleanup' EXIT INT TERM
if ! postgres-init; then
log error "postgres-init failed"
exit 1
fi
PGURL="postgresql://$(id -un)@/testdb?host=${PG_WORKING_DIR}/sock&port=5432"
export PGURL
dotenv_file=$(mktemp)
printf 'TEST_SECRET=hello-world\nQUOTED_SECRET="quoted value"\nDELIMITER_SECRET=before$ps_env$after\n' > "$dotenv_file"
if ! db-ops secrets load --dotenv-file "$dotenv_file"; then
log error "db-ops secrets load failed"
rm -f "$dotenv_file"
exit 1
fi
rm -f "$dotenv_file"
secret_value=$(psql "$PGURL" -v ON_ERROR_STOP=1 -tAc "SELECT value FROM hectic.secret WHERE key = 'TEST_SECRET';") || exit 1
[ "$secret_value" = "hello-world" ] || {
log error "expected TEST_SECRET to be loaded, got: $secret_value"
exit 1
}
quoted_value=$(psql "$PGURL" -v ON_ERROR_STOP=1 -tAc "SELECT value FROM hectic.secret WHERE key = 'QUOTED_SECRET';") || exit 1
[ "$quoted_value" = "quoted value" ] || {
log error "expected QUOTED_SECRET to strip outer quotes, got: $quoted_value"
exit 1
}
delimiter_value=$(psql "$PGURL" -v ON_ERROR_STOP=1 -tAc "SELECT value FROM hectic.secret WHERE key = 'DELIMITER_SECRET';") || exit 1
[ "$delimiter_value" = 'before$ps_env$after' ] || {
log error "expected DELIMITER_SECRET to preserve dollar-quote delimiter text, got: $delimiter_value"
exit 1
}
hectic_schema=$(psql "$PGURL" -v ON_ERROR_STOP=1 -tAc "SELECT count(*) FROM pg_namespace WHERE nspname='hectic';") || exit 1
[ "$hectic_schema" = 1 ] || {
log error "expected hectic schema to exist, got: $hectic_schema"
exit 1
}
log notice "test passed"
@@ -1,6 +1,6 @@
log notice "test case: ${WHITE}error: ambiguous command"
set +e
migrator --inherits tablename --inherits 'table name' list migrate
migrator --inherits tablename --inherits 'table name' list migrate >/tmp/migrator-arguments-1.out 2>&1
error_code=$?
set -e
@@ -12,9 +12,14 @@ elif [ "$error_code" != 2 ]; then
exit 1
fi
if ! grep -q 'ambiguous subcommand' /tmp/migrator-arguments-1.out; then
log error "test failed: ${WHITE}missing ambiguous subcommand diagnostic"
exit 1
fi
log notice "test case: ${WHITE}error: ambiguous migrate command"
set +e
migrator --inherits tablename --inherits 'table name' migrate to up
migrator --inherits tablename --inherits 'table name' migrate to up >/tmp/migrator-arguments-2.out 2>&1
error_code=$?
set -e
@@ -25,3 +30,40 @@ elif [ "$error_code" != 2 ]; then
log error "test failed: ${WHITE}unexpected error code"
exit 1
fi
if ! grep -q 'ambiguous migrate subcommand' /tmp/migrator-arguments-2.out; then
log error "test failed: ${WHITE}missing ambiguous migrate diagnostic"
exit 1
fi
log notice "test case: ${WHITE}error: init invalid argument is logged"
set +e
migrator init --wat >/tmp/migrator-arguments-3.out 2>&1
error_code=$?
set -e
if [ "$error_code" != 9 ]; then
log error "test failed: ${WHITE}expected exit code 9 for invalid init argument, got $error_code"
exit 1
fi
if ! grep -q 'init argument .*--wat.* does not exists' /tmp/migrator-arguments-3.out; then
log error "test failed: ${WHITE}missing init invalid argument diagnostic"
exit 1
fi
log notice "test case: ${WHITE}error: list invalid argument is logged with correct command name"
set +e
migrator list --wat >/tmp/migrator-arguments-4.out 2>&1
error_code=$?
set -e
if [ "$error_code" != 9 ]; then
log error "test failed: ${WHITE}expected exit code 9 for invalid list argument, got $error_code"
exit 1
fi
if ! grep -q 'list argument .*--wat.* does not exists' /tmp/migrator-arguments-4.out; then
log error "test failed: ${WHITE}missing list invalid argument diagnostic"
exit 1
fi
@@ -24,6 +24,14 @@ if [ "$answer" -ne 4 ]; then
exit 1
fi
log notice "test case: ${WHITE}unguarded assignment under errexit"
answer=$(index_of "$array" 'item4')
if [ "$answer" -ne 4 ]; then
log error "test failed: ${WHITE}wrong answer from unguarded assignment"
exit 1
fi
log notice "test case: ${WHITE}error: item not found"
if answer=$(index_of "$array" 'item10'); then
log error "test failed: ${WHITE} must return an error"