From 46e585ec928e934eed008987f916507554a20082 Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 2 Aug 2025 19:41:40 -0700 Subject: [PATCH] Clean up Nix configuration and remove old files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move Nix package and module to nix/ directory for better organization - Remove old Nix files (systant.nix, systant-old.nix, etc.) - Remove debug script and systemd service file - Update config files for new Nix structure - Add CLAUDE.md with project documentation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .gitignore | 7 +++ CLAUDE.md | 79 ++++++++++++++++++++++++ config/config.exs | 10 --- config/runtime.exs | 4 +- debug-nix-store.sh | 39 ------------ flake.lock | 61 ++++++++++++++++++ flake.nix | 29 +++++++-- nixos-module.nix => nix/nixos-module.nix | 0 systant.nix => nix/package.nix | 0 systant-manual.nix | 62 ------------------- systant-old.nix | 73 ---------------------- systant-wrapper.nix | 67 -------------------- systant.service | 26 -------- 13 files changed, 173 insertions(+), 284 deletions(-) create mode 100644 CLAUDE.md delete mode 100644 debug-nix-store.sh create mode 100644 flake.lock rename nixos-module.nix => nix/nixos-module.nix (100%) rename systant.nix => nix/package.nix (100%) delete mode 100644 systant-manual.nix delete mode 100644 systant-old.nix delete mode 100644 systant-wrapper.nix delete mode 100644 systant.service diff --git a/.gitignore b/.gitignore index c65532c..8f61012 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,10 @@ system_stats_daemon-*.tar # Temporary files, for example, from tests. /tmp/ + +# Nix direnv cache and generated files +.direnv/ + +# Nix result symlinks +result +result-* diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5ec9696 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,79 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Common Commands + +### Development +```bash +# Install dependencies +mix deps.get + +# Compile the project +mix compile + +# Run in development (non-halt mode) +mix run --no-halt + +# Run tests +mix test + +# Run specific test +mix test test/systant_test.exs + +# Enter development shell (via Nix) +nix develop +``` + +### Production +```bash +# Build production release +MIX_ENV=prod mix release + +# Run production release +_build/prod/rel/systant/bin/systant start +``` + +## Architecture Overview + +This is an Elixir OTP application that serves as a systemd daemon for MQTT-based system monitoring, designed for deployment across multiple NixOS hosts to integrate with Home Assistant. + +### Core Components +- **Systant.Application** (`lib/systant/application.ex`): OTP application supervisor that starts the MQTT client +- **Systant.MqttClient** (`lib/systant/mqtt_client.ex`): GenServer that handles MQTT connection, publishes stats every 30 seconds, and listens for commands +- **Configuration**: MQTT settings configurable via environment variables or config files + +### Key Libraries +- **Tortoise**: MQTT client library for pub/sub functionality +- **Jason**: JSON encoding/decoding for message payloads + +### MQTT Behavior +- Publishes "Hello from systant" messages with timestamp and hostname to stats topic every 30 seconds +- Subscribes to commands topic for incoming events that can trigger user-customizable actions +- Uses randomized client ID to avoid conflicts across multiple hosts +- Sends immediate hello message on startup + +### Default Configuration +- **MQTT Host**: `mqtt.home` (not localhost) +- **Stats Topic**: `systant/${hostname}/stats` (per-host topics) +- **Command Topic**: `systant/${hostname}/commands` (per-host topics) +- **Publish Interval**: 30 seconds + +### NixOS Deployment +This project includes a complete Nix packaging and NixOS module: + +- **Package**: `nix/package.nix` - Builds the Elixir release using beamPackages.mixRelease +- **Module**: `nix/nixos-module.nix` - Provides `services.systant` configuration options +- **Development**: Use `nix develop` for development shell with Elixir/Erlang + +The NixOS module supports: +- Configurable MQTT connection settings +- Per-host topic naming using `${config.networking.hostName}` +- Environment variable configuration for runtime settings +- Systemd service with security hardening +- Auto-restart and logging to systemd journal + +### Future Plans +- Integration with Home Assistant via custom MQTT integration +- Expandable command handling for host-specific automation +- Multi-host deployment for comprehensive system monitoring \ No newline at end of file diff --git a/config/config.exs b/config/config.exs index 482b639..d062f79 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,15 +1,5 @@ import Config -config :systant, Systant.MqttClient, - host: "mqtt.home", - port: 1883, - client_id: "systant", - username: "mqtt", - password: "pleasework", - stats_topic_base: "systant", # Will become systant/{hostname}/stats - command_topic_base: "systant", # Will become systant/{hostname}/commands - publish_interval: 30_000 - config :logger, :console, format: "$time $metadata[$level] $message\n", metadata: [:request_id] diff --git a/config/runtime.exs b/config/runtime.exs index 2b8ed5b..240076a 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -8,11 +8,11 @@ end # Runtime configuration that can use environment variables config :systant, Systant.MqttClient, - host: System.get_env("SYSTANT_MQTT_HOST", "localhost"), + host: System.get_env("SYSTANT_MQTT_HOST", "mqtt.home"), port: String.to_integer(System.get_env("SYSTANT_MQTT_PORT", "1883")), client_id: System.get_env("SYSTANT_CLIENT_ID", "systant"), username: System.get_env("SYSTANT_MQTT_USERNAME"), password: System.get_env("SYSTANT_MQTT_PASSWORD"), stats_topic: System.get_env("SYSTANT_STATS_TOPIC", "systant/#{hostname}/stats"), command_topic: System.get_env("SYSTANT_COMMAND_TOPIC", "systant/#{hostname}/commands"), - publish_interval: String.to_integer(System.get_env("SYSTANT_PUBLISH_INTERVAL", "30000")) \ No newline at end of file + publish_interval: String.to_integer(System.get_env("SYSTANT_PUBLISH_INTERVAL", "30000")) diff --git a/debug-nix-store.sh b/debug-nix-store.sh deleted file mode 100644 index c4e69d9..0000000 --- a/debug-nix-store.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -# Debug script to check what's in the Nix store -STORE_PATH="/nix/store/i3fa1p0y8mcyljzfsj6k65qfpmd4yyaf-systant-0.1.0" - -echo "=== Checking Nix store contents ===" -echo "Store path: $STORE_PATH" -echo - -echo "=== Top level contents ===" -ls -la "$STORE_PATH/" -echo - -echo "=== Looking for COOKIE files ===" -find "$STORE_PATH" -name "*COOKIE*" -o -name "*cookie*" 2>/dev/null || echo "No COOKIE files found" -echo - -echo "=== releases directory ===" -if [ -d "$STORE_PATH/releases" ]; then - ls -la "$STORE_PATH/releases/" -else - echo "No releases directory found" -fi -echo - -echo "=== bin directory ===" -if [ -d "$STORE_PATH/bin" ]; then - ls -la "$STORE_PATH/bin/" -else - echo "No bin directory found" -fi -echo - -echo "=== Checking startup script ===" -if [ -f "$STORE_PATH/bin/systant" ]; then - echo "Startup script exists, checking for COOKIE references:" - grep -n "COOKIE" "$STORE_PATH/bin/systant" || echo "No COOKIE references in startup script" -else - echo "No startup script found" -fi \ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..5feb92a --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1753939845, + "narHash": "sha256-K2ViRJfdVGE8tpJejs8Qpvvejks1+A4GQej/lBk5y7I=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "94def634a20494ee057c76998843c015909d6311", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix index 3584dd6..2ff2a26 100644 --- a/flake.nix +++ b/flake.nix @@ -6,16 +6,34 @@ flake-utils.url = "github:numtide/flake-utils"; }; - outputs = { self, nixpkgs, flake-utils }: - flake-utils.lib.eachDefaultSystem (system: + outputs = + { + self, + nixpkgs, + flake-utils, + }: + flake-utils.lib.eachDefaultSystem ( + system: let - pkgs = nixpkgs.legacyPackages.${system}; + pkgs = import nixpkgs { + system = system; + config.allowUnfree = true; + }; in { devShells.default = pkgs.mkShell { buildInputs = with pkgs; [ elixir erlang + claude-code + aider-chat + nodejs_20 + yarn + deno + python311 + python311Packages.pip + python311Packages.ipython + ]; shellHook = '' @@ -23,5 +41,6 @@ elixir --version ''; }; - }); -} \ No newline at end of file + } + ); +} diff --git a/nixos-module.nix b/nix/nixos-module.nix similarity index 100% rename from nixos-module.nix rename to nix/nixos-module.nix diff --git a/systant.nix b/nix/package.nix similarity index 100% rename from systant.nix rename to nix/package.nix diff --git a/systant-manual.nix b/systant-manual.nix deleted file mode 100644 index 0b316b8..0000000 --- a/systant-manual.nix +++ /dev/null @@ -1,62 +0,0 @@ -{ lib, stdenv, fetchgit, elixir, erlang, rebar3, git }: - -stdenv.mkDerivation rec { - pname = "systant"; - version = "0.1.0"; - - src = fetchgit { - url = "https://git.ryanpandya.com/ryan/systant.git"; - rev = "f8173a1afb03623039ff504c587d7322a9876f3d"; - sha256 = "sha256-1cRfSoH+JdO4a7q4hRZSkoDMk2wMCYRIyCIN56FSUgg="; - }; - - nativeBuildInputs = [ elixir erlang rebar3 git ]; - - # Disable network access - we'll handle deps manually - MIX_ENV = "prod"; - - configurePhase = '' - export MIX_HOME=$TMPDIR/mix - export HEX_HOME=$TMPDIR/hex - export REBAR_CACHE_DIR=$TMPDIR/rebar3 - - # Install hex and rebar locally - mix local.hex --force - mix local.rebar --force - ''; - - buildPhase = '' - # This will fail but that's OK - we're using it to get deps info - mix deps.get || true - - # Manual dependency installation (you'd need to add each dep here) - # For now, let's try without deps to see if it builds - mix deps.compile --skip-deps || true - mix compile - mix release - - echo "=== Build phase debug ===" - find _build/prod/rel/ -name "*COOKIE*" || echo "No COOKIE in build" - ''; - - installPhase = '' - mkdir -p $out - cp -r _build/prod/rel/systant/* $out/ - - # Force create COOKIE file - mkdir -p $out/releases - echo "systant-cookie-change-in-production" > $out/releases/COOKIE - - echo "=== Install phase debug ===" - echo "COOKIE created manually in installPhase" > /tmp/manual-nix-debug - ls -la $out/releases/ - cat $out/releases/COOKIE - ''; - - meta = with lib; { - description = "Systant - System stats MQTT daemon"; - homepage = "https://git.ryanpandya.com/ryan/systant"; - license = licenses.mit; - platforms = platforms.linux; - }; -} \ No newline at end of file diff --git a/systant-old.nix b/systant-old.nix deleted file mode 100644 index a111242..0000000 --- a/systant-old.nix +++ /dev/null @@ -1,73 +0,0 @@ -{ lib -, stdenv -, fetchgit -, elixir -, erlang -, rebar3 -, git -, cacert -, glibcLocales -}: - -stdenv.mkDerivation rec { - pname = "systant"; - version = "0.1.0"; - - src = fetchgit { - url = "https://git.ryanpandya.com/ryan/systant.git"; - rev = "92fc90e3b470dd2d11ba3a84745e33195e8e9db3"; - sha256 = lib.fakeSha256; # Replace with actual hash after first build attempt - }; - - nativeBuildInputs = [ - elixir - erlang - rebar3 - git - cacert - glibcLocales - ]; - - buildPhase = '' - runHook preBuild - - # Set up environment for Mix/Hex - export MIX_ENV=prod - export MIX_HOME=$TMPDIR/mix - export HEX_HOME=$TMPDIR/hex - export REBAR_CACHE_DIR=$TMPDIR/rebar3 - - # SSL and locale configuration - export SSL_CERT_FILE=${cacert}/etc/ssl/certs/ca-bundle.crt - export LOCALE_ARCHIVE=${glibcLocales}/lib/locale/locale-archive - export LC_ALL=en_US.UTF-8 - export ELIXIR_ERL_OPTIONS="+fnu" - - # Install hex and rebar locally - mix local.hex --force - mix local.rebar --force - - # Get dependencies and build release - mix deps.get --only=prod - mix release - - runHook postBuild - ''; - - installPhase = '' - runHook preInstall - - mkdir -p $out - cp -r _build/prod/rel/systant/* $out/ - - runHook postInstall - ''; - - meta = with lib; { - description = "Systant - System stats MQTT daemon for monitoring system metrics"; - homepage = "https://git.ryanpandya.com/ryan/systant"; - license = licenses.mit; # Update if different - maintainers = [ ]; # Add your maintainer info if desired - platforms = platforms.linux; - }; -} \ No newline at end of file diff --git a/systant-wrapper.nix b/systant-wrapper.nix deleted file mode 100644 index c9003a9..0000000 --- a/systant-wrapper.nix +++ /dev/null @@ -1,67 +0,0 @@ -{ lib, stdenv, fetchgit, elixir, erlang, rebar3, git, writeShellScript }: - -let - systantBase = stdenv.mkDerivation rec { - pname = "systant-base"; - version = "0.1.0"; - - src = fetchgit { - url = "https://git.ryanpandya.com/ryan/systant.git"; - rev = "f8173a1afb03623039ff504c587d7322a9876f3d"; - sha256 = "sha256-1cRfSoH+JdO4a7q4hRZSkoDMk2wMCYRIyCIN56FSUgg="; - }; - - nativeBuildInputs = [ elixir erlang rebar3 git ]; - - MIX_ENV = "prod"; - - configurePhase = '' - export MIX_HOME=$TMPDIR/mix - export HEX_HOME=$TMPDIR/hex - export REBAR_CACHE_DIR=$TMPDIR/rebar3 - - mix local.hex --force - mix local.rebar --force - ''; - - buildPhase = '' - mix deps.get || true - mix deps.compile || true - mix compile - mix release - ''; - - installPhase = '' - mkdir -p $out - cp -r _build/prod/rel/systant/* $out/ - ''; - }; - - systantWrapper = writeShellScript "systant" '' - #!/bin/bash - export RELEASE_COOKIE="systant-cookie-change-in-production" - exec ${systantBase}/bin/systant "$@" - ''; - -in stdenv.mkDerivation { - pname = "systant"; - version = "0.1.0"; - - buildCommand = '' - mkdir -p $out/bin - cp ${systantWrapper} $out/bin/systant - chmod +x $out/bin/systant - - # Copy the rest of the release - cp -r ${systantBase}/* $out/ - - echo "Wrapper created - RELEASE_COOKIE will be set by wrapper script" - ''; - - meta = with lib; { - description = "Systant - System stats MQTT daemon"; - homepage = "https://git.ryanpandya.com/ryan/systant"; - license = licenses.mit; - platforms = platforms.linux; - }; -} \ No newline at end of file diff --git a/systant.service b/systant.service deleted file mode 100644 index c41a143..0000000 --- a/systant.service +++ /dev/null @@ -1,26 +0,0 @@ -[Unit] -Description=Systant MQTT Daemon -After=network.target - -[Service] -Type=exec -User=root -Group=root -ExecStart=/opt/systant/bin/systant start -ExecStop=/opt/systant/bin/systant stop -Restart=always -RestartSec=5 -StandardOutput=journal -StandardError=journal -SyslogIdentifier=systant -WorkingDirectory=/opt/systant - -# Security settings - still apply restrictions where possible -NoNewPrivileges=true -PrivateTmp=true -ProtectHome=true -# Don't use ProtectSystem=strict since we need to read system stats -ProtectSystem=false - -[Install] -WantedBy=multi-user.target \ No newline at end of file