Compare commits

...

7 commits

Author SHA1 Message Date
10bad206e5 Refactor fastfetch images and terminal output
Some checks are pending
Build NixOS ISOs (x86 only) / build-iso (x86, desktop) (push) Waiting to run
Build NixOS ISOs (x86 only) / build-iso (x86, server) (push) Waiting to run
Build NixOS ISOs (x86 only) / create-release (push) Blocked by required conditions
- Update logo paths and fallback from text to PNG images
- Adjust logo settings for kitty display (dimensions, padding)
- Modify shell command in gen.fish to use an alternative runner
- Wrap fastfetch call in a fish_greeting function for terminal detection
- Revise host configuration for updated system specs
2025-06-20 17:41:33 -04:00
894cc4444c Enhances gaming sessions with gamescope commands
- Refactors monitor handling by computing primary monitor parameters
- Introduces new shell scripts for gamescope-run and steam-session integration
- Updates desktop entries to launch Steam and Heroic within gamescope
- Removes deprecated gaming session script from the global gaming configuration
- Simplifies monitor options and adds primary monitor utility in shared libraries
- Adjusts host-specific monitor configurations and README hardware details
2025-06-20 17:41:08 -04:00
12ac221b46 Update gojo hardware to correct uuids 2025-06-20 17:39:24 -04:00
4563b89b6e Much needed improvements to gaming configs; dconf additions too
- Remove heroic overrides
- Add Mangohud overlay with custom settings
- Introduce a gamescope session script for Steam with HDR and compatibility adjustments; ACTUALLY WORKS! Games run flawless
- Update desktop window properties and GNOME session defaults
- Enable Waydroid configuration in rune
2025-06-20 00:10:11 -04:00
6d0942d351 Fix rune monitors and add Heroic launcher 2025-06-19 18:58:13 -04:00
80f932ba6d Update dependencies and add Waydroid integration
- Update chaotic package URL reference
- Add steam-run package for gaming support
- Streamline GNOME session setup by removing x11
- Introduce Waydroid configuration with helper package and memory settings
2025-06-19 01:12:27 -04:00
538d0a20a4 Replaced individual backup configs with unified backup module module 2025-06-17 17:23:18 -04:00
30 changed files with 877 additions and 1030 deletions

58
flake.lock generated
View file

@ -69,46 +69,24 @@
}, },
"chaotic": { "chaotic": {
"inputs": { "inputs": {
"fenix": "fenix",
"flake-schemas": "flake-schemas", "flake-schemas": "flake-schemas",
"home-manager": "home-manager", "home-manager": "home-manager",
"jovian": "jovian", "jovian": "jovian",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}, },
"locked": { "locked": {
"lastModified": 1748304776, "lastModified": 1750195929,
"narHash": "sha256-Eb+kBcm7ECpJ1HKjMgvZPo9TpGG0CpzfGRUc0FCZKP0=", "narHash": "sha256-5gaf/9wuxtfKqAFnNlX74Vz2VMURa/UzyfuEyYv4tZw=",
"owner": "chaotic-cx", "owner": "chaotic-cx",
"repo": "nyx", "repo": "nyx",
"rev": "5278f55d2c2c568db38ed03370606b5e009e34df", "rev": "419a1cfaf34100008ff5fa97ce9ef3b194472f71",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "chaotic-cx", "owner": "chaotic-cx",
"ref": "nyxpkgs-unstable",
"repo": "nyx", "repo": "nyx",
"rev": "5278f55d2c2c568db38ed03370606b5e009e34df",
"type": "github"
}
},
"fenix": {
"inputs": {
"nixpkgs": [
"chaotic",
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1747392669,
"narHash": "sha256-zky3+lndxKRu98PAwVK8kXPdg+Q1NVAhaI7YGrboKYA=",
"owner": "nix-community",
"repo": "fenix",
"rev": "c3c27e603b0d9b5aac8a16236586696338856fbb",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github" "type": "github"
} }
}, },
@ -760,20 +738,24 @@
"type": "github" "type": "github"
} }
}, },
"rust-analyzer-src": { "rust-overlay": {
"flake": false, "inputs": {
"nixpkgs": [
"chaotic",
"nixpkgs"
]
},
"locked": { "locked": {
"lastModified": 1747323949, "lastModified": 1750214276,
"narHash": "sha256-G4NwzhODScKnXqt2mEQtDFOnI0wU3L1WxsiHX3cID/0=", "narHash": "sha256-1kniuhH70q4TAC/xIvjFYH46aHiLrbIlcr6fdrRwO1A=",
"owner": "rust-lang", "owner": "oxalica",
"repo": "rust-analyzer", "repo": "rust-overlay",
"rev": "f8e784353bde7cbf9a9046285c1caf41ac484ebe", "rev": "f9b2b2b1327ff6beab4662b8ea41689e0a57b8d4",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "rust-lang", "owner": "oxalica",
"ref": "nightly", "repo": "rust-overlay",
"repo": "rust-analyzer",
"type": "github" "type": "github"
} }
}, },

View file

@ -5,7 +5,7 @@
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
nixpkgs-stable.url = "github:NixOS/nixpkgs/nixos-25.05"; nixpkgs-stable.url = "github:NixOS/nixpkgs/nixos-25.05";
nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixos-unstable"; nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixos-unstable";
chaotic.url = "github:chaotic-cx/nyx/5278f55d2c2c568db38ed03370606b5e009e34df"; # Bleeding edge packages from Chaotic-AUR chaotic.url = "github:chaotic-cx/nyx/nyxpkgs-unstable"; # Bleeding edge packages from Chaotic-AUR
## NixOS ## ## NixOS ##

View file

@ -4,31 +4,141 @@
lib, lib,
... ...
}: }:
let
primaryMonitor = lib.custom.getPrimaryMonitor (config.monitors or [ ]);
WIDTH = primaryMonitor.width or 1980;
HEIGHT = primaryMonitor.height or 1080;
REFRESH_RATE = primaryMonitor.refreshRate or 60;
VRR = primaryMonitor.vrr or false;
HDR = primaryMonitor.hdr or false;
# INFO: Example working commands for running games in steam-session
## Rivals ##
# SteamDeck=1 LD_PRELOAD="" PROTON_ENABLE_NVAPI=1 PROTON_ENABLE_WAYLAND=1 VKD3D_DISABLE_EXTENSIONS=VK_KHR_present_wait gamemoderun %command% -PSOCompileMode=1 -dx12
## Stats Overlay ##
# gamemoderun mangohud %command%
## gamescope-run ##
# GAMESCOPE_EXTRA_OPTS="--force-grab-cursor"; gamescope-run gamemoderun mangohud %command%
gamescope-base-script = pkgs.writeShellScript "gamescope-base" ''
# Session configuration
export CAP_SYS_NICE="eip"
export DXVK_HDR="1"
export ENABLE_GAMESCOPE_WSI="1"
export ENABLE_HDR_WSI="1"
export AMD_VULKAN_ICD=RADV
export RADV_PERFTEST=aco
export SDL_VIDEODRIVER="wayland"
# Steam environment variables for better compatibility
export STEAM_FORCE_DESKTOPUI_SCALING=1
export STEAM_GAMEPADUI=1
export STEAM_GAMESCOPE_CLIENT=1
# Default resolution settings
WIDTH=${toString WIDTH}
HEIGHT=${toString HEIGHT}
REFRESH_RATE=${toString REFRESH_RATE}
# Build gamescope options
GAMESCOPE_OPTS="--fade-out-duration 200 -w $WIDTH -h $HEIGHT -r $REFRESH_RATE -f"
GAMESCOPE_OPTS="$GAMESCOPE_OPTS --expose-wayland --backend sdl --rt --immediate-flips"
${lib.optionalString HDR ''GAMESCOPE_OPTS="$GAMESCOPE_OPTS --hdr-enabled --hdr-debug-force-output --hdr-itm-enable"''}
${lib.optionalString VRR ''GAMESCOPE_OPTS="$GAMESCOPE_OPTS --adaptive-sync"''}
# Allow extra options to be passed
GAMESCOPE_OPTS="$GAMESCOPE_OPTS $GAMESCOPE_EXTRA_OPTS"
# Execute gamescope with provided command
exec ${lib.getExe pkgs.gamescope} $GAMESCOPE_OPTS -- "$@"
'';
gamescope-run = pkgs.writeShellScriptBin "gamescope-run" ''
if [ $# -eq 0 ]; then
echo "Usage: gamescope-run <command> [args...]"
echo "Examples:"
echo " gamescope-run heroic"
echo " gamescope-run steam"
echo " gamescope-run lutris"
exit 1
fi
exec ${gamescope-base-script} "$@"
'';
steam-session-script = pkgs.writeShellScript "steam-session-script" ''
# Check launch mode parameter
case "$1" in
"desktop")
# Launch Steam in desktop mode within gamescope
exec ${gamescope-base-script} ${lib.getExe pkgs.steam}
;;
"bigpicture"|*)
# Default: Launch Steam in Big Picture mode
exec ${gamescope-base-script} ${lib.getExe pkgs.steam} -tenfoot -steamdeck -gamepadui
;;
esac
'';
in
{ {
imports = lib.custom.scanPaths ./.; imports = lib.custom.scanPaths ./.;
home.packages = with pkgs; [ home.packages = with pkgs; [
prismlauncher prismlauncher
steam-run
heroic
gamescope-run
# modrinth-app # modrinth-app
]; ];
programs.mangohud = {
enable = true;
settings = {
position = "top-right";
cpu_stats = true;
gpu_stats = true;
fps = true;
font_size = 12;
cellpadding_y = -0.070;
background_alpha = lib.mkForce 0.5;
alpha = lib.mkForce 0.75;
};
};
xdg.desktopEntries = {
steam = {
name = "Steam Desktop (Gamescope)";
comment = "Steam in Gamescope Session";
exec = "${steam-session-script} desktop";
icon = "steam";
type = "Application";
terminal = false;
categories = [ "Game" ];
actions = {
desktop = {
name = "Steam Big Picture (Gamescope)";
exec = "${steam-session-script} bigpicture";
};
regular = {
name = "Steam Desktop (No Gamescope)";
exec = "${lib.getExe pkgs.steam}";
};
};
};
"com.heroicgameslauncher.hgl.desktop" = {
name = "Heroic Games Launcher (Gamescope)";
comment = "Heroic in Gamescope Session";
exec = "${lib.getExe gamescope-run} heroic";
icon = "com.heroicgameslauncher.hgl";
type = "Application";
terminal = false;
categories = [ "Game" ];
actions = {
regular = {
name = "Heroic (No Gamescope)";
exec = "${lib.getExe pkgs.heroic}";
};
};
};
};
} }
# INFO: Example working commands for running gamescope, excuse the mess lol
# gamescope --adaptive-sync --backend sdl --expose-wayland --force-grab-cursor --framerate-limit 120 --immediate-flips --output-height 2160 --output-width 3840 --prefer-output DP-3 --rt -- gamemoderun %command%
# AMD_VULKAN_ICD=RADV RADV_PERFTEST=aco PROTON_USE_D9VK=1 CAP_SYS_NICE=eip gamescope --expose-wayland --backend sdl --framerate-limit 120 --output-height 2160 --output-width 3840 --prefer-vk-device 7550:C0 --rt -F fsr -f --sharpness 4 -- gamemoderun %command%
# CAP_SYS_NICE=eip gamescope --expose-wayland --backend sdl --framerate-limit 144 --output-height 1440 --output-width 2560 --rt -F fsr -f --sharpness 4 -- gamemoderun %command%
# gamescope --expose-wayland --backend sdl --framerate-limit 144 --output-height 1440 --output-width 2560 --prefer-vk-device 7480:CF --prefer-output DP-2 --fullscreen --rt -F fsr --sharpness 4 -- gamemoderun %command%
# CAP_SYS_NICE=eip gamescope --expose-wayland --backend sdl --framerate-limit 160 --output-height 1440 --output-width 2560 --prefer-vk-device 73A5:C0 --prefer-output DP-2 --fullscreen --force-windows-fullscreen --rt -F fsr --sharpness 4 -- gamemoderun %command%
# SDL_VIDEODRIVER=x11 CAP_SYS_NICE=eip gamescope --expose-wayland --backend sdl --framerate-limit 160 --output-height 1440 --output-width 2560 --force-windows-fullscreen -f --prefer-vk-device 73A5:C0 --prefer-output DP-2 -- gamemoderun %command% --use-d3d11
# SteamDeck=1 LD_PRELOAD="" PROTON_ENABLE_NVAPI=1 PROTON_ENABLE_WAYLAND=1 VKD3D_DISABLE_EXTENSIONS=VK_KHR_present_wait gamemoderun %command% -PSOCompileMode=1
#SDL_VIDEODRIVER=x11 AMD_VULKAN_ICD=RADV RADV_PERFTEST=aco PROTON_USE_D9VK=1 CAP_SYS_NICE=eip gamescope --expose-wayland --backend sdl --framerate-limit 120 --output-height 2160 --output-width 3840 --prefer-vk-device 7550:C0 --rt -F fsr -f --sharpness 4 -- gamemoderun %command%
#SDL_VIDEODRIVER=x11 AMD_VULKAN_ICD=RADV RADV_PERFTEST=aco PROTON_USE_D9VK=1 CAP_SYS_NICE=eip gamescope --expose-wayland --backend sdl --framerate-limit 120 --output-height 2160 --output-width 3840 --prefer-vk-device 7550:C0 --force-windows-fullscreen -f --prefer-vk-device 73A5:C0 --prefer-output DP-2 -- gamemoderun %command% --use-d3d11

View file

@ -322,9 +322,12 @@ with lib.hm.gvariant;
vertical-margin-bottom = 8; vertical-margin-bottom = 8;
window-gap = 8; window-gap = 8;
winprops = [ winprops = [
'' ''
{"wm_class":"Code","spaceIndex":0} {"wm_class":"Code","spaceIndex":0}
'' ''
''
{"wm_class":"com.jaoushingan.WaydroidHelper","scratch_layer":true}
''
'' ''
{"wm_class":"com.mitchellh.ghostty","scratch_layer":true} {"wm_class":"com.mitchellh.ghostty","scratch_layer":true}
'' ''
@ -337,12 +340,21 @@ with lib.hm.gvariant;
'' ''
{"wm_class":"gnome-extensions-app","scratch_layer":true} {"wm_class":"gnome-extensions-app","scratch_layer":true}
'' ''
''
{"wm_class":"org.gnome.Extensions","scratch_layer":true}
''
'' ''
{"wm_class":"org.gnome.Nautilus","scratch_layer":true} {"wm_class":"org.gnome.Nautilus","scratch_layer":true}
'' ''
'' ''
{"wm_class":"TelegramDesktop","spaceIndex":1} {"wm_class":"TelegramDesktop","spaceIndex":1}
'' ''
''
{"wm_class":"Waydroid","preferredWidth":"100%","spaceIndex":0,"title":""}
''
''
{"wm_class":".gamescope-wrapped","preferredWidth":"100%","spaceIndex":2}
''
]; ];
}; };

View file

@ -6,15 +6,14 @@
... ...
}: }:
{ {
#TODO: Scripts might need a rework
programs.fastfetch = programs.fastfetch =
let let
hostname = hostSpec.hostName; hostname = hostSpec.hostName;
logoFile = logoFile =
let let
hostLogoPath = ./. + "/host/${hostname}.txt"; hostLogoPath = ./. + "/host/images/${hostname}.png";
in in
if builtins.pathExists hostLogoPath then hostLogoPath else ./host/nix.txt; if builtins.pathExists hostLogoPath then hostLogoPath else ./host/images/nix.png;
weather = import ./scripts/weather.nix { inherit pkgs; }; weather = import ./scripts/weather.nix { inherit pkgs; };
title = import ./scripts/title.nix { inherit pkgs; }; title = import ./scripts/title.nix { inherit pkgs; };
in in
@ -22,12 +21,14 @@
enable = true; enable = true;
settings = { settings = {
logo = { logo = {
source = builtins.readFile logoFile; type = "kitty";
type = "data"; source = logoFile;
position = "left"; width = 21; # columns
height = 12; # rows
padding = { padding = {
top = 0; top = 1;
right = 0; right = 2;
left = 2;
}; };
}; };
display = { display = {
@ -72,7 +73,11 @@
type = "wm"; type = "wm";
} }
{ {
text = "printf '%s%s' (string upper (string sub -l 1 $SHELL)) (string lower (string sub -s 2 $SHELL))"; text =
let
name = lib.getName pkgs.fish;
in
"printf '%s%s' (string upper (string sub -l 1 ${name})) (string lower (string sub -s 2 ${name}))";
key = "shell » {#keys}"; key = "shell » {#keys}";
keyColor = "1;33"; keyColor = "1;33";
type = "command"; type = "command";

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 123 KiB

View file

@ -21,7 +21,10 @@ for i in (seq 1 $num)
set type2 $types[$j] set type2 $types[$j]
set combination "$type1+$type2" set combination "$type1+$type2"
echo "Creating with type $combination" echo "Creating with type $combination"
nix run nixpkgs#chafa -- -s 24x11 -w 9 --symbols $combination --view-size 24x11 $input_png yay try chafa -- chafa -s 24x11 -w 9 --symbols $combination --view-size 24x11 $input_png
# chafa -s 23x12 -w 9 --stretch --symbols $combination --view-size 23x12 $input_png # yay try chafa -- chafa -s 23x12 -w 9 --stretch --symbols $combination --view-size 23x12 $input_png
end end
end end
# yay try chafa -- chafa -s 23x12 -w 9 --symbols half --view-size 23x12 ./1989.png
# fastfetch --logo-height 12 --logo-width 21 --logo-type kitty --logo-position left --logo-preserve-aspect-ratio --logo ./haze.png --logo-padding 2 --logo-padding-top 1

View file

@ -1,4 +1,10 @@
set fish_greeting # Disable greeting function fish_greeting
if test "$SIXEL" = true; or string match -q xterm-256color "$TERM"
fastfetch --logo-type sixel
else
fastfetch
end
end
## Aliases and Overrides ## ## Aliases and Overrides ##
@ -108,7 +114,3 @@ function unzipz
return 1 return 1
end end
end end
## Fish Prompt ##
fastfetch

View file

@ -2,10 +2,11 @@
{ {
imports = lib.custom.scanPaths ./.; imports = lib.custom.scanPaths ./.;
# home.file.".config/monitors_source" = { # home.file.".config/monitors_source" = {
# source = ./monitors.xml; # source = ./monitors.xml;
# onChange = '' # onChange = ''
# cp $HOME/.config/monitors_source $HOME/.config/monitors.xml # cp $HOME/.config/monitors_source $HOME/.config/monitors.xml
# ''; # chmod 755 $HOME/.config/monitors.xml
# }; # '';
# };
} }

View file

@ -30,10 +30,45 @@
}; };
}; };
monitors = [
{
name = "DP-2";
primary = true;
width = 3840;
height = 2160;
refreshRate = 60;
x = 0;
y = 0;
scale = 1.0;
transform = 0;
enabled = true;
hdr = true;
vrr = true;
}
{
name = "HDMI-1";
primary = false;
width = 1920;
height = 1080;
refreshRate = 60;
x = 1920; # Positioned to the right of DP-1
y = 0;
scale = 1.0;
transform = 1;
enabled = true;
}
];
## TODO:
## I want to automate this monitors.xml
## But dk how to do it properly yet.
## So for now ill still use this for the gnome config
home.file.".config/monitors_source" = { home.file.".config/monitors_source" = {
source = ./monitors.xml; source = ./monitors.xml;
onChange = '' onChange = ''
cp $HOME/.config/monitors_source $HOME/.config/monitors.xml cp $HOME/.config/monitors_source $HOME/.config/monitors.xml
chmod 755 $HOME/.config/monitors.xml
''; '';
}; };
} }

View file

@ -1,28 +1,6 @@
<monitors version="2"> <monitors version="2">
<configuration> <configuration>
<layoutmode>physical</layoutmode> <layoutmode>logical</layoutmode>
<logicalmonitor>
<x>0</x>
<y>0</y>
<scale>1</scale>
<transform>
<rotation>right</rotation>
<flipped>no</flipped>
</transform>
<monitor>
<monitorspec>
<connector>HDMI-1-2</connector>
<vendor>DEL</vendor>
<product>DELL U2417H</product>
<serial>XVNNT67I176L</serial>
</monitorspec>
<mode>
<width>1920</width>
<height>1080</height>
<rate>60.000</rate>
</mode>
</monitor>
</logicalmonitor>
<logicalmonitor> <logicalmonitor>
<x>1080</x> <x>1080</x>
<y>0</y> <y>0</y>
@ -42,5 +20,27 @@
</mode> </mode>
</monitor> </monitor>
</logicalmonitor> </logicalmonitor>
<logicalmonitor>
<x>0</x>
<y>0</y>
<scale>1</scale>
<transform>
<rotation>right</rotation>
<flipped>no</flipped>
</transform>
<monitor>
<monitorspec>
<connector>HDMI-2</connector>
<vendor>DEL</vendor>
<product>DELL U2417H</product>
<serial>XVNNT67I176L</serial>
</monitorspec>
<mode>
<width>1920</width>
<height>1080</height>
<rate>60.000</rate>
</mode>
</monitor>
</logicalmonitor>
</configuration> </configuration>
</monitors> </monitors>

View file

@ -2,22 +2,33 @@
pkgs, pkgs,
lib, lib,
config, config,
inputs,
... ...
}: }:
{ {
hardware.opengl = {
hardware.graphics = {
enable = true; enable = true;
driSupport32Bit = true; enable32Bit = true;
}; };
# AMDgpu tool # AMDgpu tool
environment.systemPackages = with pkgs; [ lact ]; environment.systemPackages = with pkgs; [
systemd.packages = with pkgs; [ lact ]; lact
systemd.services.lactd.wantedBy = [ "multi-user.target" ]; gamescope
# gamescope-wsi
];
systemd = {
packages = with pkgs; [ lact ];
services.lactd.wantedBy = [ "multi-user.target" ];
};
programs = { programs = {
steam = { steam = {
enable = true; enable = true;
remotePlay.openFirewall = true;
dedicatedServer.openFirewall = true;
protontricks = { protontricks = {
enable = true; enable = true;
package = pkgs.protontricks; package = pkgs.protontricks;
@ -41,7 +52,7 @@
inherit (pkgs) inherit (pkgs)
gamemode gamemode
gamescope # !!!: DO NOT ADD GAMESCOPE ANYWHERE ELSE IN CONFIG, IT WILL BREAK! mangohud
gperftools gperftools
keyutils keyutils
libkrb5 libkrb5

View file

@ -1,14 +1,11 @@
{ pkgs, config, ... }: {
config,
lib,
pkgs,
...
}:
{ {
## DE ## ## DE ##
services.xserver = {
enable = true;
xkb = {
layout = "us";
variant = "";
};
};
services.desktopManager.gnome = { services.desktopManager.gnome = {
enable = true; enable = true;
extraGSettingsOverridePackages = [ pkgs.mutter ]; extraGSettingsOverridePackages = [ pkgs.mutter ];
@ -23,12 +20,25 @@
enable = true; enable = true;
wayland = true; wayland = true;
}; };
# Set the custom session as default
defaultSession = lib.mkForce "gnome";
autoLogin = { autoLogin = {
enable = true; enable = true;
user = config.hostSpec.username; user = config.hostSpec.username;
}; };
}; };
# Configure keyboard layout for Wayland
services.xserver = {
enable = false;
xkb = {
layout = "us";
variant = "";
};
};
#INFO: Fix for autoLogin #INFO: Fix for autoLogin
systemd.services."getty@tty1".enable = false; systemd.services."getty@tty1".enable = false;
systemd.services."autovt@tty1".enable = false; systemd.services."autovt@tty1".enable = false;
@ -53,7 +63,6 @@
]; ];
## Exclusions ## ## Exclusions ##
services.xserver.excludePackages = [ pkgs.xterm ];
environment.gnome.excludePackages = ( environment.gnome.excludePackages = (
with pkgs; with pkgs;
[ [

View file

@ -0,0 +1,44 @@
{ pkgs, ... }:
let
# TODO: automate with monitors.nix when i fix that up again
width = 1064;
height = 1904;
in
{
# Enable Waydroid
virtualisation.waydroid.enable = true;
# Required packages
environment.systemPackages = with pkgs; [
waydroid
waydroid-helper
wl-clipboard # Clipboard sharing
# Waydroid setup script; run once to initialize
(pkgs.writeShellScriptBin "waydroid-setup" ''
#!/usr/bin/env bash
set -e
echo "Initializing WayDroid with GApps support..."
sudo waydroid init -s GAPPS -f
echo "Setting default WayDroid properties..."
sudo mkdir -p /var/lib/waydroid
# Create or update waydroid_base.prop
echo "persist.waydroid.multi_windows=true" | sudo tee /var/lib/waydroid/waydroid_base.prop
echo "persist.waydroid.width=${toString width}" | sudo tee -a /var/lib/waydroid/waydroid_base.prop
echo "persist.waydroid.height=${toString height}" | sudo tee -a /var/lib/waydroid/waydroid_base.prop
echo "sys.use_memfd=true" | sudo tee -a /var/lib/waydroid/waydroid_base.prop
echo "Setup complete!"
echo ""
echo "To start WayDroid:"
echo " 1. sudo systemctl start waydroid-container"
echo " 2. waydroid session start"
echo " 3. waydroid show-full-ui"
echo ""
echo "To install APKs: waydroid app install /path/to/app.apk"
'')
];
}

View file

@ -0,0 +1,43 @@
{
config,
lib,
...
}:
{
services.backup = {
enable = true;
notificationUrl = lib.custom.mkAppriseUrl config.secretsSpec.users.admin.smtp "relay@ryot.foo";
enableChainTimer = true;
chainSchedule = "*-*-* 03:00:00";
# Maintenance jobs run first
maintenanceJobs = [
{
name = "snapraid";
title = "SnapRAID";
service = "snapraid-aio.service";
logPattern = "SnapRAID-*.out";
logPath = "/var/log/snapraid";
}
];
# Backup jobs run after maintenance
jobs = [
{
name = "forgejo";
title = "Forgejo";
repo = "/pool/Backups/forgejo";
sourcePath = "/pool/forgejo";
verbose = true;
}
{
name = "docker-storage";
title = "Docker Storage";
repo = "/pool/Backups/DockerStorage";
sourcePath = "/mnt/drive1/DockerStorage";
verbose = true;
}
];
};
}

View file

@ -1,207 +0,0 @@
{
config,
lib,
pkgs,
...
}:
let
# Common repositories
dockerStorageRepo = "/pool/Backups/DockerStorage";
forgejoRepo = "/pool/Backups/forgejo";
# Shared environment setup
borgCommonSettings = ''
# Don't use cache to avoid issues with concurrent backups
export BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=yes
export BORG_NON_INTERACTIVE=yes
'';
# Common packages needed for backups
commonBorgPath = with pkgs; [
borgbackup
coreutils
apprise
gnugrep
hostname
util-linux
gawk
];
# Repository initialization
initRepo = repo: ''
if [ ! -d "${repo}" ]; then
mkdir -p "${repo}"
${pkgs.borgbackup}/bin/borg init --encryption=none "${repo}"
fi
'';
# Notification system
apprise-url = config.secretsSpec.users.admin.smtp.notifyUrl;
sendNotification = title: message: ''
${pkgs.apprise}/bin/apprise -t "${title}" -b "${message}" "${apprise-url}" || true
'';
# Statistics generation
extractBorgStats = logFile: repoPath: ''
{
echo -e "\n==== BACKUP SUMMARY ====\n"
grep -A10 "Archive name:" ${logFile} || echo "No archive stats found"
echo -e "\n=== Compression ===\n"
grep "Compressed size:" ${logFile} || echo "No compression stats found"
echo -e "\n=== Duration ===\n"
grep "Duration:" ${logFile} || echo "No duration stats found"
grep "Throughput:" ${logFile} || echo "No throughput stats found"
echo -e "\n=== Repository ===\n"
${pkgs.borgbackup}/bin/borg info ${repoPath} --last 1 2>/dev/null || echo "Could not get repository info"
echo -e "\n=== Storage Space ===\n"
df -h ${repoPath} | grep -v "Filesystem" || echo "Could not get storage info"
} > ${logFile}.stats
STATS=$(cat ${logFile}.stats || echo "No stats available")
'';
# Unified backup service generator with optional features
mkBorgBackupService =
{
name,
title,
repo,
sourcePath,
keepDaily,
keepWeekly,
keepMonthly,
schedule ? null,
enableNotifications ? true,
verbose ? false,
}:
let
maybeCreateTimer = lib.optionalAttrs (schedule != null) {
timers."backup-${name}" = {
description = "Timer for ${title} Backup";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = schedule;
Persistent = true;
RandomizedDelaySec = "5min";
};
};
};
logPrefix = if verbose then "set -x;" else "";
in
{
services."backup-${name}" = {
description = "Backup ${title} with Borg";
inherit (commonServiceConfig) path serviceConfig;
script = ''
${borgCommonSettings}
${logPrefix} # Add verbose logging if enabled
LOG_FILE="/tmp/borg-${name}-backup-$(date +%Y%m%d-%H%M%S).log"
${initRepo repo}
echo "Starting ${title} backup at $(date)" > $LOG_FILE
ARCHIVE_NAME="${name}-$(date +%Y-%m-%d_%H%M%S)"
START_TIME=$(date +%s)
# Add verbose output redirection if enabled
${if verbose then "exec 3>&1 4>&2" else ""}
${pkgs.borgbackup}/bin/borg create \
--stats \
--compression zstd,15 \
--exclude '*.tmp' \
--exclude '*/tmp/*' \
${repo}::$ARCHIVE_NAME \
${sourcePath} >> $LOG_FILE 2>&1 ${if verbose then "| tee /dev/fd/3" else ""}
BACKUP_STATUS=$?
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
echo "Total time: $DURATION seconds ($(date -d@$DURATION -u +%H:%M:%S))" >> $LOG_FILE
${extractBorgStats "$LOG_FILE" "${repo}"}
echo -e "\nPruning old backups..." >> $LOG_FILE
${pkgs.borgbackup}/bin/borg prune \
--keep-daily ${toString keepDaily} \
--keep-weekly ${toString keepWeekly} \
--keep-monthly ${toString keepMonthly} \
${repo} >> $LOG_FILE 2>&1 ${if verbose then "| tee /dev/fd/3" else ""}
PRUNE_STATUS=$?
echo -e "\nRemaining archives after pruning:" >> $LOG_FILE
${pkgs.borgbackup}/bin/borg list ${repo} >> $LOG_FILE 2>&1 || true
${
if enableNotifications then
''
if [ $BACKUP_STATUS -eq 0 ] && [ $PRUNE_STATUS -eq 0 ]; then
${sendNotification " ${title} Backup Complete" "${title} backup completed successfully on $(hostname) at $(date)\nDuration: $(date -d@$DURATION -u +%H:%M:%S)\n\n$STATS"}
else
${sendNotification " ${title} Backup Failed" "${title} backup failed on $(hostname) at $(date)\n\nBackup Status: $BACKUP_STATUS\nPrune Status: $PRUNE_STATUS\n\nPartial Stats:\n$STATS\n\nSee $LOG_FILE for details"}
fi
''
else
"echo 'Notifications disabled' >> $LOG_FILE"
}
rm -f $LOG_FILE.stats
exit $BACKUP_STATUS
'';
};
}
// maybeCreateTimer;
# Common service configuration
commonServiceConfig = {
path = commonBorgPath;
serviceConfig = {
Type = "oneshot";
IOSchedulingClass = "idle";
CPUSchedulingPolicy = "idle";
Nice = 19;
};
};
in
{
environment.systemPackages = with pkgs; [
borgbackup
];
systemd = lib.mkMerge [
(mkBorgBackupService {
name = "docker-storage";
title = "Docker Storage";
repo = dockerStorageRepo;
sourcePath = "/mnt/drive1/DockerStorage";
# INFO: This shit confusing but basically
# keeps the last 7 days,
# then keeps AT LEAST ONE for last 4 weeks
# and finally AT LEAST ONE for the last 3 months
keepDaily = 7;
keepWeekly = 4;
keepMonthly = 3;
# No schedule = no timer created
# schedule = "*-*-* 03:00:00";
enableNotifications = false;
verbose = true;
})
(mkBorgBackupService {
name = "forgejo";
title = "Forgejo";
repo = forgejoRepo;
sourcePath = "/pool/forgejo";
keepDaily = 7;
keepWeekly = 4;
keepMonthly = 3;
# schedule = "*-*-* 03:00:00";
enableNotifications = false;
verbose = true;
})
];
}

View file

@ -1,197 +0,0 @@
{
config,
lib,
pkgs,
...
}:
let
# Shared configuration
logDir = "/var/log/backups";
backupServices = [
{
name = "forgejo";
title = "Forgejo";
service = "backup-forgejo.service";
logPattern = "borg-forgejo-backup-*.log";
}
{
name = "docker_storage";
title = "Docker Storage";
service = "backup-docker-storage.service";
logPattern = "borg-docker-storage-backup-*.log";
}
{
name = "snapraid";
title = "SnapRAID";
service = "snapraid-aio.service";
logPattern = "SnapRAID-*.out";
logPath = "/var/log/snapraid";
}
];
# Helper functions
users = config.secretsSpec.users;
notify =
title: message: logFile:
let
attachArg = if logFile == "" then "" else "--attach \"file://${logFile}\"";
appriseUrl = lib.custom.mkAppriseUrl users.admin.smtp "relay@ryot.foo";
in
''
${pkgs.apprise}/bin/apprise -vv -i "markdown" -t "${title}" \
-b "${message}" \
${attachArg} \
"${appriseUrl}" || true
'';
findLatestLog = pattern: path: ''
find "${path}" -name "${pattern}" -type f -printf "%T@ %p\\n" 2>/dev/null \
| sort -nr | head -1 | cut -d' ' -f2
'';
# Generate safe variable name (replace hyphens with underscores)
safeName = name: lib.replaceStrings [ "-" ] [ "_" ] name;
# Generate status variable references
statusVarName = name: "STATUS_${safeName name}";
# Common script utilities
scriptPrelude = ''
set -uo pipefail
LOG_FILE="${logDir}/backup-chain-$(date +%Y%m%d-%H%M%S).log"
mkdir -p "${logDir}"
exec > >(tee -a "$LOG_FILE") 2>&1
log() {
echo "[$(date "+%Y-%m-%d %H:%M:%S")] $1"
}
# Initialize all status variables
${lib.concatMapStringsSep "\n" (s: "${statusVarName s.name}=1") backupServices}
'';
# Service runner template
runService =
{
name,
title,
service,
logPattern,
logPath ? "/tmp",
}:
''
log "Starting ${title} maintenance..."
systemctl start ${service} || true
${statusVarName name}=$?
log "${title} completed with status $${statusVarName name}"
SERVICE_LOG=$(${findLatestLog logPattern logPath})
if [ -n "$SERVICE_LOG" ]; then
log "Appending ${title} log: $SERVICE_LOG"
echo -e "\n\n===== ${title} LOG ($(basename "$SERVICE_LOG")) =====\n" >> "$LOG_FILE"
cat "$SERVICE_LOG" >> "$LOG_FILE"
# Add SnapRAID-specific summary
if [ "${name}" = "snapraid" ]; then
echo -e "\n=== SnapRAID Summary ===" >> "$LOG_FILE"
grep -E '(Scrub|Sync|Diff|smart)' "$SERVICE_LOG" | tail -n 10 >> "$LOG_FILE"
fi
fi
'';
# Build the service execution script
serviceExecution = lib.concatMapStrings runService backupServices;
# Generate status summary lines
statusSummaryLines = lib.concatMapStringsSep "\n" (
s:
let
varName = statusVarName s.name;
in
"- **${s.title}:** \$([ \$${varName} -eq 0 ] && echo ' Success' || echo ' Failed') (Exit: \$${varName})"
) backupServices;
# Notification logic with cleaner formatting
notificationLogic =
let
statusVars = map (s: statusVarName s.name) backupServices;
statusChecks = lib.concatMapStringsSep "\n" (var: "[ \$${var} -eq 0 ] && ") statusVars;
in
''
# Calculate overall status
OVERALL_STATUS=0
${lib.concatMapStringsSep "\n" (var: "if [ \$${var} -ne 0 ]; then OVERALL_STATUS=1; fi") statusVars}
TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
HOSTNAME=$(hostname)
SUMMARY=$(cat << EOF
# Backup Chain Complete
**Host:** $HOSTNAME
**Timestamp:** $TIMESTAMP
**Overall Status:** $([ $OVERALL_STATUS -eq 0 ] && echo ' Success' || echo ' Failure')
## Service Status:
${statusSummaryLines}
**Log Path:** $LOG_FILE
EOF)
if [ $OVERALL_STATUS -eq 0 ]; then
${notify " Backup Success" "$SUMMARY" "$LOG_FILE"}
else
${notify " Backup Issues" "$SUMMARY" "$LOG_FILE"}
fi
exit $OVERALL_STATUS
'';
in
{
imports = lib.custom.scanPaths ./.;
systemd.services.backup-chain = {
description = "Orchestrated Backup Chain";
path = with pkgs; [
apprise
coreutils
findutils
gawk
gnugrep
hostname
systemd
util-linux
];
serviceConfig = {
Type = "oneshot";
Nice = 19;
IOSchedulingClass = "idle";
CPUSchedulingPolicy = "idle";
};
script = ''
${scriptPrelude}
log "Initializing backup chain on $(hostname)"
${serviceExecution}
log "Finalizing backup chain"
${notificationLogic}
'';
};
systemd.timers.backup-chain = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "*-*-* 03:00:00";
Persistent = true;
RandomizedDelaySec = "5min";
};
};
environment.systemPackages = [ pkgs.apprise ];
systemd.tmpfiles.rules = [ "d ${logDir} 0755 root root -" ];
}

View file

@ -1,92 +0,0 @@
{
pkgs,
inputs,
config,
lib,
modulesPath,
...
}:
{
imports = lib.flatten [
(modulesPath + "/installer/scan/not-detected.nix")
];
## Boot ##
boot = {
loader = {
systemd-boot = {
enable = true;
# When using plymouth, initrd can expand by a lot each time, so limit how many we keep around
configurationLimit = lib.mkDefault 10;
};
efi.canTouchEfiVariables = true;
timeout = 3;
};
# Use the cachyos kernel for better performance
kernelPackages = pkgs.linuxPackages_cachyos;
initrd = {
systemd.enable = true;
verbose = false;
availableKernelModules = [
"nvme"
"xhci_pci"
"ahci"
"usb_storage"
"usbhid"
"sd_mod"
];
kernelModules = [ ];
};
kernelModules = [
"kvm-amd"
"amdgpu"
];
extraModulePackages = [ ];
};
# For less permission issues with SSHFS
programs.fuse.userAllowOther = true;
# FIXME: Fix on first boot
# fileSystems = {
# "/" = {
# device = "/dev/disk/by-uuid/d38c182c-6f05-4bf3-8a45-5532c10fd342";
# fsType = "ext4";
# };
# "/boot" = {
# device = "/dev/disk/by-uuid/5B39-A7CB";
# fsType = "vfat";
# options = [
# "fmask=0077"
# "dmask=0077"
# ];
# };
# };
# FIXME: Fix on first boot
# swapDevices = [ { device = "/dev/disk/by-uuid/6586847d-eba9-4317-9077-98ae9b2812c9"; } ];
time.hardwareClockInLocalTime = true; # Fixes windows dual-boot time issues
# Enables DHCP on each ethernet and wireless interface. In case of scripted networking
# (the default) this is the recommended approach. When using systemd-networkd it's
# still possible to use this option, but it's recommended to use it in conjunction
# with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`.
# networking.useDHCP = lib.mkDefault true;
# networking.interfaces.enp5s0.useDHCP = lib.mkDefault true;
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableAllFirmware;
}
# STUFF ABOUT CHAOTIC NIX CACHE
# nix eval 'github:chaotic-cx/nyx/nyxpkgs-unstable#linuxPackages_cachyos.kernel.outPath'
# nix eval 'chaotic#linuxPackages_cachyos.kernel.outPath'
# nix eval '/pool/git/Nix/dot.nix#nixosConfigurations.rune.config.boot.kernelPackages.kernel.outPath'
# curl -L 'https://chaotic-nyx.cachix.org/{{HASH}}.narinfo'
# sudo nixos-rebuild switch --flake ./git/Nix/dot.nix/. --option 'extra-substituters' 'https://chaotic-nyx.cachix.org/' --option extra-trusted-public-keys "chaotic-nyx.cachix.org-1:HfnXSw4pj95iI/n17rIDy40agHj12WfF+Gqk6SonIT8="

View file

@ -1,64 +1,90 @@
# FIXME: FIX to hardware.fix once out of VM, this is TEMP vm hardware config
{ {
pkgs,
inputs,
config, config,
lib, lib,
pkgs,
modulesPath, modulesPath,
... ...
}: }:
let
username = config.hostSpec.username;
in
{ {
imports = lib.flatten [ imports = lib.flatten [
(modulesPath + "/profiles/qemu-guest.nix") (modulesPath + "/installer/scan/not-detected.nix")
]; ];
## Boot ## ## Boot ##
boot = { boot = {
loader = { loader = {
grub = { systemd-boot = {
enable = true; enable = true;
device = "/dev/vda"; # When using plymouth, initrd can expand by a lot each time, so limit how many we keep around
useOSProber = true; configurationLimit = lib.mkDefault 10;
}; };
efi.canTouchEfiVariables = true; efi.canTouchEfiVariables = true;
timeout = 3; timeout = 3;
}; };
# use latest kernel # Use the cachyos kernel for better performance
kernelPackages = pkgs.linuxPackages_latest; kernelPackages = pkgs.linuxPackages_cachyos;
initrd = { initrd = {
availableKernelModules = [
"ahci"
"xhci_pci"
"virtio_pci"
"sr_mod"
"virtio_blk"
];
systemd.enable = true; systemd.enable = true;
verbose = false; verbose = false;
availableKernelModules = [
"nvme"
"xhci_pci"
"ahci"
"usb_storage"
"usbhid"
"sd_mod"
];
kernelModules = [ ];
}; };
kernelModules = [ "kvm-amd" ]; kernelModules = [
"kvm-amd"
"amdgpu"
];
extraModulePackages = [ ]; extraModulePackages = [ ];
}; };
# For less permission issues with SSHFS
programs.fuse.userAllowOther = true;
fileSystems."/" = { fileSystems."/" = {
device = "/dev/disk/by-uuid/5f1ad3a9-18ce-42ab-83ea-b67bccaa6972"; device = "/dev/disk/by-uuid/a19362ba-329b-4688-be05-c180f4212d97";
fsType = "ext4"; fsType = "ext4";
}; };
fileSystems."/boot" = {
device = "/dev/disk/by-uuid/8E6A-D6AE";
fsType = "vfat";
options = [
"fmask=0077"
"dmask=0077"
];
};
swapDevices = [ swapDevices = [
{ device = "/dev/disk/by-uuid/e3fc8d25-31a5-48c1-8c81-c6c237f671bb"; } { device = "/dev/disk/by-uuid/2c3c3142-93c9-4be7-b9c1-5e9bc45d9fff"; }
]; ];
time.hardwareClockInLocalTime = true; # Fixes windows dual-boot time issues
# Enables DHCP on each ethernet and wireless interface. In case of scripted networking # Enables DHCP on each ethernet and wireless interface. In case of scripted networking
# (the default) this is the recommended approach. When using systemd-networkd it's # (the default) this is the recommended approach. When using systemd-networkd it's
# still possible to use this option, but it's recommended to use it in conjunction # still possible to use this option, but it's recommended to use it in conjunction
# with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`. # with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`.
# networking.useDHCP = lib.mkDefault true; # networking.useDHCP = lib.mkDefault true;
# networking.interfaces.enp1s0.useDHCP = lib.mkDefault true; # networking.interfaces.enp5s0.useDHCP = lib.mkDefault true;
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableAllFirmware;
} }
# STUFF ABOUT CHAOTIC NIX CACHE
# nix eval 'github:chaotic-cx/nyx/nyxpkgs-unstable#linuxPackages_cachyos.kernel.outPath'
# nix eval 'chaotic#linuxPackages_cachyos.kernel.outPath'
# nix eval '/pool/git/Nix/dot.nix#nixosConfigurations.rune.config.boot.kernelPackages.kernel.outPath'
# curl -L 'https://chaotic-nyx.cachix.org/{{HASH}}.narinfo'
# sudo nixos-rebuild switch --flake ./git/Nix/dot.nix/. --option 'extra-substituters' 'https://chaotic-nyx.cachix.org/' --option extra-trusted-public-keys "chaotic-nyx.cachix.org-1:HfnXSw4pj95iI/n17rIDy40agHj12WfF+Gqk6SonIT8="

View file

@ -1,7 +1,7 @@
############################################################### ###############################################################
# #
# Haze - Cesar's Desktop # Haze - Cesar's Desktop
# NixOS running on Ryzen 7 ___ , Radeon RX 6950 XT, 32GB RAM # NixOS running on Ryzen 5 7600x, Radeon RX 7600, 32GB RAM
# #
############################################################### ###############################################################

View file

@ -43,6 +43,7 @@ in
"hosts/global/common/nvtop.nix" # GPU monitor (not available in home-manager) "hosts/global/common/nvtop.nix" # GPU monitor (not available in home-manager)
"hosts/global/common/plymouth.nix" # fancy boot screen "hosts/global/common/plymouth.nix" # fancy boot screen
"hosts/global/common/solaar.nix" # Logitech Unifying Receiver support "hosts/global/common/solaar.nix" # Logitech Unifying Receiver support
"hosts/global/common/waydroid.nix" # Android container
"hosts/global/common/vial.nix" # KB setup "hosts/global/common/vial.nix" # KB setup
]) ])
]; ];

View file

@ -0,0 +1,23 @@
{
config,
lib,
pkgs,
...
}:
{
services.backup = {
enable = true;
notificationUrl = lib.custom.mkAppriseUrl config.secretsSpec.users.admin.smtp "relay@ryot.foo";
enableChainTimer = true;
jobs = [
{
name = "ochre-storage";
title = "Ochre Storage";
repo = "/pool/Backups/OchreStorage";
sourcePath = "/OchreStorage";
}
];
};
}

View file

@ -1,193 +0,0 @@
{
config,
lib,
pkgs,
...
}:
let
# Common repositories
ochreStorageRepo = "/pool/Backups/OchreStorage";
# Shared environment setup
borgCommonSettings = ''
# Don't use cache to avoid issues with concurrent backups
export BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=yes
export BORG_NON_INTERACTIVE=yes
'';
# Common packages needed for backups
commonBorgPath = with pkgs; [
borgbackup
coreutils
apprise
gnugrep
hostname
util-linux
gawk
];
# Repository initialization
initRepo = repo: ''
if [ ! -d "${repo}" ]; then
mkdir -p "${repo}"
${pkgs.borgbackup}/bin/borg init --encryption=none "${repo}"
fi
'';
# Notification system
apprise-url = config.secretsSpec.users.admin.smtp.notifyUrl;
sendNotification = title: message: ''
${pkgs.apprise}/bin/apprise -t "${title}" -b "${message}" "${apprise-url}" || true
'';
# Statistics generation
extractBorgStats = logFile: repoPath: ''
{
echo -e "\n==== BACKUP SUMMARY ====\n"
grep -A10 "Archive name:" ${logFile} || echo "No archive stats found"
echo -e "\n=== Compression ===\n"
grep "Compressed size:" ${logFile} || echo "No compression stats found"
echo -e "\n=== Duration ===\n"
grep "Duration:" ${logFile} || echo "No duration stats found"
grep "Throughput:" ${logFile} || echo "No throughput stats found"
echo -e "\n=== Repository ===\n"
${pkgs.borgbackup}/bin/borg info ${repoPath} --last 1 2>/dev/null || echo "Could not get repository info"
echo -e "\n=== Storage Space ===\n"
df -h ${repoPath} | grep -v "Filesystem" || echo "Could not get storage info"
} > ${logFile}.stats
STATS=$(cat ${logFile}.stats || echo "No stats available")
'';
# Unified backup service generator with optional features
mkBorgBackupService =
{
name,
title,
repo,
sourcePath,
keepDaily,
keepWeekly,
keepMonthly,
schedule ? null,
enableNotifications ? true,
verbose ? false,
}:
let
maybeCreateTimer = lib.optionalAttrs (schedule != null) {
timers."backup-${name}" = {
description = "Timer for ${title} Backup";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = schedule;
Persistent = true;
RandomizedDelaySec = "5min";
};
};
};
logPrefix = if verbose then "set -x;" else "";
in
{
services."backup-${name}" = {
description = "Backup ${title} with Borg";
inherit (commonServiceConfig) path serviceConfig;
script = ''
${borgCommonSettings}
${logPrefix} # Add verbose logging if enabled
LOG_FILE="/tmp/borg-${name}-backup-$(date +%Y%m%d-%H%M%S).log"
${initRepo repo}
echo "Starting ${title} backup at $(date)" > $LOG_FILE
ARCHIVE_NAME="${name}-$(date +%Y-%m-%d_%H%M%S)"
START_TIME=$(date +%s)
# Add verbose output redirection if enabled
${if verbose then "exec 3>&1 4>&2" else ""}
${pkgs.borgbackup}/bin/borg create \
--stats \
--compression zstd,15 \
--exclude '*.tmp' \
--exclude '*/tmp/*' \
${repo}::$ARCHIVE_NAME \
${sourcePath} >> $LOG_FILE 2>&1 ${if verbose then "| tee /dev/fd/3" else ""}
BACKUP_STATUS=$?
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
echo "Total time: $DURATION seconds ($(date -d@$DURATION -u +%H:%M:%S))" >> $LOG_FILE
${extractBorgStats "$LOG_FILE" "${repo}"}
echo -e "\nPruning old backups..." >> $LOG_FILE
${pkgs.borgbackup}/bin/borg prune \
--keep-daily ${toString keepDaily} \
--keep-weekly ${toString keepWeekly} \
--keep-monthly ${toString keepMonthly} \
${repo} >> $LOG_FILE 2>&1 ${if verbose then "| tee /dev/fd/3" else ""}
PRUNE_STATUS=$?
echo -e "\nRemaining archives after pruning:" >> $LOG_FILE
${pkgs.borgbackup}/bin/borg list ${repo} >> $LOG_FILE 2>&1 || true
${
if enableNotifications then
''
if [ $BACKUP_STATUS -eq 0 ] && [ $PRUNE_STATUS -eq 0 ]; then
${sendNotification " ${title} Backup Complete" "${title} backup completed successfully on $(hostname) at $(date)\nDuration: $(date -d@$DURATION -u +%H:%M:%S)\n\n$STATS"}
else
${sendNotification " ${title} Backup Failed" "${title} backup failed on $(hostname) at $(date)\n\nBackup Status: $BACKUP_STATUS\nPrune Status: $PRUNE_STATUS\n\nPartial Stats:\n$STATS\n\nSee $LOG_FILE for details"}
fi
''
else
"echo 'Notifications disabled' >> $LOG_FILE"
}
rm -f $LOG_FILE.stats
exit $BACKUP_STATUS
'';
};
}
// maybeCreateTimer;
# Common service configuration
commonServiceConfig = {
path = commonBorgPath;
serviceConfig = {
Type = "oneshot";
IOSchedulingClass = "idle";
CPUSchedulingPolicy = "idle";
Nice = 19;
};
};
in
{
environment.systemPackages = with pkgs; [
borgbackup
];
systemd = lib.mkMerge [
(mkBorgBackupService {
name = "ochre-storage";
title = "Ochre Storage";
repo = ochreStorageRepo;
sourcePath = "/OchreStorage";
# INFO: This shit confusing but basically
# keeps the last 7 days,
# then keeps AT LEAST ONE for last 4 weeks
# and finally AT LEAST ONE for the last 3 months
keepDaily = 7;
keepWeekly = 4;
keepMonthly = 3;
# No schedule = no timer created
# schedule = "*-*-* 03:00:00";
enableNotifications = false;
verbose = true;
})
];
}

View file

@ -1,184 +0,0 @@
{
config,
lib,
pkgs,
...
}:
let
# Shared configuration
logDir = "/var/log/backups";
backupServices = [
{
name = "ochre_storage";
title = "Ochre Storage";
service = "backup-ochre-storage.service";
logPattern = "borg-ochre-storage-backup-*.log";
}
];
# Helper functions
users = config.secretsSpec.users;
notify =
title: message: logFile:
let
attachArg = if logFile == "" then "" else "--attach \"file://${logFile}\"";
appriseUrl = lib.custom.mkAppriseUrl users.admin.smtp "relay@ryot.foo";
in
''
${pkgs.apprise}/bin/apprise -vv -i "markdown" -t "${title}" \
-b "${message}" \
${attachArg} \
"${appriseUrl}" || true
'';
findLatestLog = pattern: path: ''
find "${path}" -name "${pattern}" -type f -printf "%T@ %p\\n" 2>/dev/null \
| sort -nr | head -1 | cut -d' ' -f2
'';
# Generate safe variable name (replace hyphens with underscores)
safeName = name: lib.replaceStrings [ "-" ] [ "_" ] name;
# Generate status variable references
statusVarName = name: "STATUS_${safeName name}";
# Common script utilities
scriptPrelude = ''
set -uo pipefail
LOG_FILE="${logDir}/backup-chain-$(date +%Y%m%d-%H%M%S).log"
mkdir -p "${logDir}"
exec > >(tee -a "$LOG_FILE") 2>&1
log() {
echo "[$(date "+%Y-%m-%d %H:%M:%S")] $1"
}
# Initialize all status variables
${lib.concatMapStringsSep "\n" (s: "${statusVarName s.name}=1") backupServices}
'';
# Service runner template
runService =
{
name,
title,
service,
logPattern,
logPath ? "/tmp",
}:
''
log "Starting ${title} maintenance..."
systemctl start ${service} || true
${statusVarName name}=$?
log "${title} completed with status $${statusVarName name}"
SERVICE_LOG=$(${findLatestLog logPattern logPath})
if [ -n "$SERVICE_LOG" ]; then
log "Appending ${title} log: $SERVICE_LOG"
echo -e "\n\n===== ${title} LOG ($(basename "$SERVICE_LOG")) =====\n" >> "$LOG_FILE"
cat "$SERVICE_LOG" >> "$LOG_FILE"
# Add SnapRAID-specific summary
if [ "${name}" = "snapraid" ]; then
echo -e "\n=== SnapRAID Summary ===" >> "$LOG_FILE"
grep -E '(Scrub|Sync|Diff|smart)' "$SERVICE_LOG" | tail -n 10 >> "$LOG_FILE"
fi
fi
'';
# Build the service execution script
serviceExecution = lib.concatMapStrings runService backupServices;
# Generate status summary lines
statusSummaryLines = lib.concatMapStringsSep "\n" (
s:
let
varName = statusVarName s.name;
in
"- **${s.title}:** \$([ \$${varName} -eq 0 ] && echo ' Success' || echo ' Failed') (Exit: \$${varName})"
) backupServices;
# Notification logic with cleaner formatting
notificationLogic =
let
statusVars = map (s: statusVarName s.name) backupServices;
statusChecks = lib.concatMapStringsSep "\n" (var: "[ \$${var} -eq 0 ] && ") statusVars;
in
''
# Calculate overall status
OVERALL_STATUS=0
${lib.concatMapStringsSep "\n" (var: "if [ \$${var} -ne 0 ]; then OVERALL_STATUS=1; fi") statusVars}
TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
HOSTNAME=$(hostname)
SUMMARY=$(cat << EOF
# Backup Chain Complete
**Host:** $HOSTNAME
**Timestamp:** $TIMESTAMP
**Overall Status:** $([ $OVERALL_STATUS -eq 0 ] && echo ' Success' || echo ' Failure')
## Service Status:
${statusSummaryLines}
**Log Path:** $LOG_FILE
EOF)
if [ $OVERALL_STATUS -eq 0 ]; then
${notify " Backup Success" "$SUMMARY" "$LOG_FILE"}
else
${notify " Backup Issues" "$SUMMARY" "$LOG_FILE"}
fi
exit $OVERALL_STATUS
'';
in
{
imports = lib.custom.scanPaths ./.;
systemd.services.backup-chain = {
description = "Orchestrated Backup Chain";
path = with pkgs; [
apprise
coreutils
findutils
gawk
gnugrep
hostname
systemd
util-linux
];
serviceConfig = {
Type = "oneshot";
Nice = 19;
IOSchedulingClass = "idle";
CPUSchedulingPolicy = "idle";
};
script = ''
${scriptPrelude}
log "Initializing backup chain on $(hostname)"
${serviceExecution}
log "Finalizing backup chain"
${notificationLogic}
'';
};
systemd.timers.backup-chain = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "*-*-* 03:00:00";
Persistent = true;
RandomizedDelaySec = "5min";
};
};
environment.systemPackages = [ pkgs.apprise ];
systemd.tmpfiles.rules = [ "d ${logDir} 0755 root root -" ];
}

View file

@ -34,4 +34,18 @@
to = if builtins.isAttrs smtp then recipient else smtp.user; to = if builtins.isAttrs smtp then recipient else smtp.user;
in in
"mailtos://_?user=${smtpUser}&pass=${smtpPass}&smtp=${smtpHost}&from=${smtpFrom}&to=${to}"; "mailtos://_?user=${smtpUser}&pass=${smtpPass}&smtp=${smtpHost}&from=${smtpFrom}&to=${to}";
# Get the primary monitor from a list of monitors
# Falls back to first monitor if no primary is set
getPrimaryMonitor =
monitors:
let
primaryMonitors = builtins.filter (m: m.primary or false) monitors;
in
if builtins.length primaryMonitors > 0 then
builtins.head primaryMonitors
else if builtins.length monitors > 0 then
builtins.head monitors
else
null;
} }

View file

@ -13,10 +13,6 @@
type = lib.types.bool; type = lib.types.bool;
default = false; default = false;
}; };
noBar = lib.mkOption {
type = lib.types.bool;
default = false;
};
width = lib.mkOption { width = lib.mkOption {
type = lib.types.int; type = lib.types.int;
example = 1920; example = 1920;
@ -44,22 +40,21 @@
transform = lib.mkOption { transform = lib.mkOption {
type = lib.types.int; type = lib.types.int;
default = 0; default = 0;
description = "Screen orientation: 0 = landscape, 1 = portrait left, 2 = portrait right, 3 = landscape flipped";
}; };
enabled = lib.mkOption { enabled = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = true; default = true;
}; };
workspace = lib.mkOption { hdr = lib.mkOption {
type = lib.types.nullOr lib.types.str; type = lib.types.bool;
description = "Defines a workspace that should persist on this monitor."; default = false;
default = null;
}; };
vrr = lib.mkOption { vrr = lib.mkOption {
type = lib.types.int; type = lib.types.bool;
description = "Variable Refresh Rate aka Adaptive Sync aka AMD FreeSync.\nValues are oriented towards hyprland's vrr values which are:\n0 = off, 1 = on, 2 = fullscreen only\nhttps://wiki.hyprland.org/Configuring/Variables/#misc"; description = "Variable Refresh Rate aka Adaptive Sync aka AMD FreeSync.";
default = 0; default = false;
}; };
}; };
} }
); );

404
modules/nixos/backups.nix Normal file
View file

@ -0,0 +1,404 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.services.backup;
# Helper functions
safeName = name: replaceStrings [ "-" ] [ "_" ] name;
statusVar = name: "STATUS_${safeName name}";
findLatestLog = pattern: path: ''
find "${path}" -name "${pattern}" -type f -printf "%T@ %p\\n" 2>/dev/null \
| sort -nr | head -1 | cut -d' ' -f2
'';
# Borg service generator
mkBorgService =
job:
let
borgCommon = ''
export BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=yes
export BORG_NON_INTERACTIVE=yes
'';
initRepo = ''
if [ ! -d "${job.repo}" ]; then
mkdir -p "${job.repo}"
${pkgs.borgbackup}/bin/borg init --encryption=none "${job.repo}"
fi
'';
extractStats = ''
{
echo -e "\n==== BACKUP SUMMARY ====\n"
grep -A10 "Archive name:" $LOG_FILE || echo "No archive stats found"
echo -e "\n=== Compression ===\n"
grep "Compressed size:" $LOG_FILE || echo "No compression stats found"
echo -e "\n=== Duration ===\n"
grep "Duration:" $LOG_FILE || echo "No duration stats found"
grep "Throughput:" $LOG_FILE || echo "No throughput stats found"
echo -e "\n=== Repository ===\n"
${pkgs.borgbackup}/bin/borg info ${job.repo} --last 1 2>/dev/null || echo "Could not get repository info"
echo -e "\n=== Storage Space ===\n"
df -h ${job.repo} | grep -v "Filesystem" || echo "Could not get storage info"
} > $LOG_FILE.stats
STATS=$(cat $LOG_FILE.stats || echo "No stats available")
'';
excludeArgs = concatMapStringsSep " " (pattern: "--exclude '${pattern}'") job.excludePatterns;
in
{
services."backup-${job.name}" = {
description = "Backup ${job.title} with Borg";
path = with pkgs; [
borgbackup
coreutils
gnugrep
hostname
util-linux
gawk
];
serviceConfig = {
Type = "oneshot";
IOSchedulingClass = "idle";
CPUSchedulingPolicy = "idle";
Nice = 19;
};
script = ''
${borgCommon}
${optionalString job.verbose "set -x"}
LOG_FILE="/tmp/borg-${job.name}-backup-$(date +%Y%m%d-%H%M%S).log"
${initRepo}
echo "Starting ${job.title} backup at $(date)" > $LOG_FILE
START_TIME=$(date +%s)
${pkgs.borgbackup}/bin/borg create \
--stats \
--compression ${job.compression} \
${excludeArgs} \
${job.repo}::${job.name}-$(date +%Y-%m-%d_%H%M%S) \
${job.sourcePath} >> $LOG_FILE 2>&1
BACKUP_STATUS=$?
echo "Total time: $(($(date +%s) - START_TIME)) seconds" >> $LOG_FILE
${extractStats}
echo -e "\nPruning old backups..." >> $LOG_FILE
${pkgs.borgbackup}/bin/borg prune \
--keep-daily ${toString job.keepDaily} \
--keep-weekly ${toString job.keepWeekly} \
--keep-monthly ${toString job.keepMonthly} \
${job.repo} >> $LOG_FILE 2>&1
PRUNE_STATUS=$?
rm -f $LOG_FILE.stats
exit $BACKUP_STATUS
'';
};
}
// optionalAttrs (job.schedule != null) {
timers."backup-${job.name}" = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = job.schedule;
Persistent = true;
RandomizedDelaySec = "5min";
};
};
};
# Orchestration service
mkChainService =
let
allJobs = cfg.jobs ++ cfg.maintenanceJobs;
initVars = concatMapStringsSep "\n" (j: "${statusVar j.name}=1") allJobs;
runJob = job: ''
# Determine log pattern
LOG_PATTERN="${if job.logPattern != "" then job.logPattern else "borg-${job.name}-backup-*.log"}"
log "Starting ${job.title}..."
systemctl start ${if job ? service then job.service else "backup-${job.name}"} || true
# Wait for service to complete and get its exit status
while systemctl is-active ${
if job ? service then job.service else "backup-${job.name}"
} >/dev/null 2>&1; do
sleep 5
done
# Get the actual service exit status
${statusVar job.name}=$(systemctl show ${
if job ? service then job.service else "backup-${job.name}"
} --property=ExecMainStatus --value)
log "${job.title} completed with status $${statusVar job.name}"
# Give logs time to be written
sleep 2
SERVICE_LOG=$(${findLatestLog "$LOG_PATTERN" "${job.logPath}"})
if [ -n "$SERVICE_LOG" ] && [ -r "$SERVICE_LOG" ]; then
log "Appending ${job.title} log: $SERVICE_LOG"
echo -e "\n\n===== ${job.title} LOG =====\n" >> "$LOG_FILE"
cat "$SERVICE_LOG" >> "$LOG_FILE" 2>/dev/null || echo "Could not read log file" >> "$LOG_FILE"
else
log "No readable log found for ${job.title} (pattern: $LOG_PATTERN in ${job.logPath})"
fi
'';
in
{
services.backup-chain = {
description = "Backup Orchestration Chain";
path = with pkgs; [
apprise
coreutils
findutils
gnugrep
hostname
systemd
util-linux
];
serviceConfig = {
Type = "oneshot";
Nice = 19;
IOSchedulingClass = "idle";
CPUSchedulingPolicy = "idle";
};
script = ''
set -uo pipefail
LOG_FILE="${cfg.logDir}/backup-chain-$(date +%Y%m%d-%H%M%S).log"
mkdir -p "${cfg.logDir}"
exec > >(tee -a "$LOG_FILE") 2>&1
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"; }
# Initialize status tracking
declare -A JOB_STATUS
${concatMapStrings (j: "JOB_STATUS['${j.name}']=1\n") allJobs}
log "Starting backup chain on $(hostname)"
${concatMapStrings (job: ''
log "Starting ${job.title}..."
systemctl start ${if job ? service then job.service else "backup-${job.name}"} || true
# Wait for completion
while systemctl is-active ${
if job ? service then job.service else "backup-${job.name}"
} >/dev/null 2>&1; do
sleep 5
done
# Capture exit status
JOB_STATUS['${job.name}']=$(systemctl show ${
if job ? service then job.service else "backup-${job.name}"
} --property=ExecMainStatus --value)
log "${job.title} completed with status ''${JOB_STATUS['${job.name}']}"
# Give logs time to be written
sleep 2
# Find and append logs
LOG_PATTERN="${if job.logPattern != "" then job.logPattern else "borg-${job.name}-backup-*.log"}"
SERVICE_LOG=$(${findLatestLog "$LOG_PATTERN" "${job.logPath}"})
if [ -n "$SERVICE_LOG" ] && [ -r "$SERVICE_LOG" ]; then
log "Appending ${job.title} log: $SERVICE_LOG"
echo -e "\n\n===== ${job.title} LOG =====\n" >> "$LOG_FILE"
cat "$SERVICE_LOG" >> "$LOG_FILE" 2>/dev/null || echo "Could not read log file" >> "$LOG_FILE"
else
log "No readable log found for ${job.title} (pattern: $LOG_PATTERN in ${job.logPath})"
fi
'') allJobs}
# Calculate overall status
OVERALL_STATUS=0
for status in "''${JOB_STATUS[@]}"; do
if [ "$status" -ne 0 ]; then
OVERALL_STATUS=1
break
fi
done
# Build summary
HOSTNAME=$(hostname)
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
# Build job status lines
JOB_LINES=""
${concatMapStrings (job: ''
if [ ''${JOB_STATUS['${job.name}']} -eq 0 ]; then
STATUS_ICON=""
else
STATUS_ICON=""
fi
JOB_LINES="$JOB_LINES- **${job.title}:** $STATUS_ICON (Exit: ''${JOB_STATUS['${job.name}']})
"
'') allJobs}
# Create the final summary
SUMMARY="# Backup Chain Complete
**Host:** $HOSTNAME
**Timestamp:** $TIMESTAMP
**Overall Status:** $([ $OVERALL_STATUS -eq 0 ] && echo ' Success' || echo ' Failure')
## Job Status:
$JOB_LINES
**Log Path:** $LOG_FILE"
# Send notification
${pkgs.apprise}/bin/apprise -vv -i "markdown" \
-t "Backup $([ $OVERALL_STATUS -eq 0 ] && echo '' || echo '')" \
-b "$SUMMARY" \
--attach "file://$LOG_FILE" \
"${cfg.notificationUrl}" || true
exit $OVERALL_STATUS
'';
};
timers.backup-chain = mkIf cfg.enableChainTimer {
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = cfg.chainSchedule;
Persistent = true;
RandomizedDelaySec = "5min";
};
};
};
# Job types
borgJobType = types.submodule {
options = {
name = mkOption {
type = types.str;
description = "Unique service name";
};
title = mkOption {
type = types.str;
description = "Human-readable title";
};
repo = mkOption {
type = types.str;
description = "Borg repository path";
};
sourcePath = mkOption {
type = types.str;
description = "Path to back up";
};
schedule = mkOption {
type = types.nullOr types.str;
default = null;
description = "Timer schedule for standalone execution";
};
keepDaily = mkOption {
type = types.int;
default = 7;
};
keepWeekly = mkOption {
type = types.int;
default = 4;
};
keepMonthly = mkOption {
type = types.int;
default = 3;
};
verbose = mkOption {
type = types.bool;
default = false;
};
compression = mkOption {
type = types.str;
default = "zstd,15";
};
excludePatterns = mkOption {
type = types.listOf types.str;
default = [
"*.tmp"
"*/tmp/*"
];
};
logPattern = mkOption {
type = types.str;
default = "";
description = "Log pattern for chain to find";
};
logPath = mkOption {
type = types.str;
default = "/tmp";
description = "Path to search for logs";
};
};
};
maintenanceJobType = types.submodule {
options = {
name = mkOption { type = types.str; };
title = mkOption { type = types.str; };
service = mkOption {
type = types.str;
description = "Systemd service to run";
};
logPattern = mkOption { type = types.str; };
logPath = mkOption {
type = types.str;
default = "/tmp";
};
};
};
in
{
options.services.backup = {
enable = mkEnableOption "backup system";
notificationUrl = mkOption { type = types.str; };
logDir = mkOption {
type = types.str;
default = "/var/log/backups";
};
jobs = mkOption {
type = types.listOf borgJobType;
default = [ ];
};
maintenanceJobs = mkOption {
type = types.listOf maintenanceJobType;
default = [ ];
};
enableChainTimer = mkOption {
type = types.bool;
default = false;
};
chainSchedule = mkOption {
type = types.str;
default = "*-*-* 03:00:00";
};
};
config = mkIf cfg.enable {
environment.systemPackages = with pkgs; [
borgbackup
apprise
];
systemd = mkMerge [
# Create log directory
{ tmpfiles.rules = [ "d ${cfg.logDir} 0755 root root -" ]; }
# Individual backup services
(mkMerge (map mkBorgService cfg.jobs))
# Backup chain orchestration
(mkIf (cfg.jobs != [ ] || cfg.maintenanceJobs != [ ]) mkChainService)
];
};
}

View file

@ -90,7 +90,7 @@ Each system in `hosts/nixos/<hostname>/` contains:
| ---------- | ------------- | ---------------------- | --------------------------- | -------------------------------- | | ---------- | ------------- | ---------------------- | --------------------------- | -------------------------------- |
| **rune** | Desktop | My workstation | Ryzen 9 7900X3D, RX 9070 XT | Gaming, Development, VMs | | **rune** | Desktop | My workstation | Ryzen 9 7900X3D, RX 9070 XT | Gaming, Development, VMs |
| **gojo** | Desktop | Giovanni's workstation | Ryzen 7 7800X3D, RX 7900 XT | Gaming, Development | | **gojo** | Desktop | Giovanni's workstation | Ryzen 7 7800X3D, RX 7900 XT | Gaming, Development |
| **haze** | Desktop | Cesar's workstation | Ryzen 7, RX 6950 XT | Gaming, Development | | **haze** | Desktop | Cesar's workstation | Ryzen 5 7600x, RX 7600 | Gaming, Development |
| **caenus** | Server | Oracle VPS | ARM 4vCPU, 24GB RAM, 200GB | FRP, Public IP | | **caenus** | Server | Oracle VPS | ARM 4vCPU, 24GB RAM, 200GB | FRP, Public IP |
| **sock** | Server | Backup & Storage | Intel N150 | Komodo (Docker), Backups, Newt | | **sock** | Server | Backup & Storage | Intel N150 | Komodo (Docker), Backups, Newt |
| **cloud** | LXC Container | Storage & NFS | 4C/4GB | File storage, NFS, Newt | | **cloud** | LXC Container | Storage & NFS | 4C/4GB | File storage, NFS, Newt |