mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-03-28 17:42:48 +01:00
nix flake (#577)
This commit is contained in:
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
@@ -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": 1766070988,
|
||||||
|
"narHash": "sha256-G/WVghka6c4bAzMhTwT2vjLccg/awmHkdKSd2JrycLc=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "c6245e83d836d0433170a16eb185cefe0572f8b8",
|
||||||
|
"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
|
||||||
|
}
|
||||||
353
flake.nix
Normal file
353
flake.nix
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
{
|
||||||
|
description = "PotatoMesh - A federated, Meshtastic-powered node dashboard";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, flake-utils }:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
|
||||||
|
# Python environment for the ingestor
|
||||||
|
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
|
||||||
|
meshtastic
|
||||||
|
requests
|
||||||
|
]);
|
||||||
|
|
||||||
|
# Web app wrapper script
|
||||||
|
webApp = pkgs.writeShellApplication {
|
||||||
|
name = "potato-mesh-web";
|
||||||
|
runtimeInputs = [ pkgs.ruby pkgs.bundler pkgs.sqlite pkgs.git pkgs.gnumake pkgs.gcc ];
|
||||||
|
text = ''
|
||||||
|
BASEDIR="''${XDG_DATA_HOME:-$HOME/.local/share}/potato-mesh"
|
||||||
|
WORKDIR="$BASEDIR/web"
|
||||||
|
mkdir -p "$WORKDIR"
|
||||||
|
|
||||||
|
# Copy app files if not present or outdated
|
||||||
|
APP_SRC="${./web}"
|
||||||
|
DATA_SRC="${./data}"
|
||||||
|
if [ ! -f "$WORKDIR/.installed" ] || [ "$APP_SRC" != "$(cat "$WORKDIR/.src_path" 2>/dev/null)" ]; then
|
||||||
|
# Copy web app
|
||||||
|
cp -rT "$APP_SRC" "$WORKDIR/"
|
||||||
|
chmod -R u+w "$WORKDIR"
|
||||||
|
# Copy data directory (contains SQL schemas)
|
||||||
|
mkdir -p "$BASEDIR/data"
|
||||||
|
cp -rT "$DATA_SRC" "$BASEDIR/data/"
|
||||||
|
chmod -R u+w "$BASEDIR/data"
|
||||||
|
echo "$APP_SRC" > "$WORKDIR/.src_path"
|
||||||
|
rm -f "$WORKDIR/.installed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$WORKDIR"
|
||||||
|
|
||||||
|
# Install gems if needed
|
||||||
|
if [ ! -f ".installed" ]; then
|
||||||
|
bundle config set --local path 'vendor/bundle'
|
||||||
|
bundle install
|
||||||
|
touch .installed
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec bundle exec ruby app.rb -p "''${PORT:-41447}" -o "''${HOST:-0.0.0.0}"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Ingestor wrapper script
|
||||||
|
ingestor = pkgs.writeShellApplication {
|
||||||
|
name = "potato-mesh-ingestor";
|
||||||
|
runtimeInputs = [ pythonEnv ];
|
||||||
|
text = ''
|
||||||
|
# The ingestor needs to run from parent directory with data/ folder
|
||||||
|
BASEDIR="''${XDG_DATA_HOME:-$HOME/.local/share}/potato-mesh"
|
||||||
|
if [ ! -d "$BASEDIR/data" ]; then
|
||||||
|
mkdir -p "$BASEDIR"
|
||||||
|
cp -rT "${./data}" "$BASEDIR/data/"
|
||||||
|
chmod -R u+w "$BASEDIR/data"
|
||||||
|
fi
|
||||||
|
cd "$BASEDIR"
|
||||||
|
exec python -m data.mesh
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
in {
|
||||||
|
packages = {
|
||||||
|
web = webApp;
|
||||||
|
ingestor = ingestor;
|
||||||
|
default = webApp;
|
||||||
|
};
|
||||||
|
|
||||||
|
apps = {
|
||||||
|
web = {
|
||||||
|
type = "app";
|
||||||
|
program = "${webApp}/bin/potato-mesh-web";
|
||||||
|
};
|
||||||
|
ingestor = {
|
||||||
|
type = "app";
|
||||||
|
program = "${ingestor}/bin/potato-mesh-ingestor";
|
||||||
|
};
|
||||||
|
default = self.apps.${system}.web;
|
||||||
|
};
|
||||||
|
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
buildInputs = [
|
||||||
|
pkgs.ruby
|
||||||
|
pkgs.bundler
|
||||||
|
pythonEnv
|
||||||
|
pkgs.sqlite
|
||||||
|
];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
echo "PotatoMesh development shell"
|
||||||
|
echo " - Ruby: $(ruby --version)"
|
||||||
|
echo " - Python: $(python --version)"
|
||||||
|
echo ""
|
||||||
|
echo "To run the web app: cd web && bundle install && ./app.sh"
|
||||||
|
echo "To run the ingestor: cd data && python mesh.py"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
) // {
|
||||||
|
# NixOS module
|
||||||
|
nixosModules.default = { config, lib, pkgs, ... }:
|
||||||
|
let
|
||||||
|
cfg = config.services.potato-mesh;
|
||||||
|
in {
|
||||||
|
options.services.potato-mesh = {
|
||||||
|
enable = lib.mkEnableOption "PotatoMesh web dashboard";
|
||||||
|
|
||||||
|
package = lib.mkOption {
|
||||||
|
type = lib.types.package;
|
||||||
|
default = self.packages.${pkgs.system}.web;
|
||||||
|
description = "The potato-mesh web package to use";
|
||||||
|
};
|
||||||
|
|
||||||
|
port = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
default = 41447;
|
||||||
|
description = "Port to listen on";
|
||||||
|
};
|
||||||
|
|
||||||
|
host = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "0.0.0.0";
|
||||||
|
description = "Host to bind to";
|
||||||
|
};
|
||||||
|
|
||||||
|
apiToken = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
description = "Shared secret that authorizes ingestors and API clients making POST requests. Warning: visible in nix store. Prefer apiTokenFile for production.";
|
||||||
|
};
|
||||||
|
|
||||||
|
apiTokenFile = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.path;
|
||||||
|
default = null;
|
||||||
|
description = "File containing API_TOKEN=<secret> (recommended for production)";
|
||||||
|
};
|
||||||
|
|
||||||
|
instanceDomain = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
description = "Public hostname used for metadata, federation, and generated API links";
|
||||||
|
};
|
||||||
|
|
||||||
|
siteName = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "PotatoMesh Demo";
|
||||||
|
description = "Title and header displayed in the UI";
|
||||||
|
};
|
||||||
|
|
||||||
|
channel = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "#LongFast";
|
||||||
|
description = "Default channel name displayed in the UI";
|
||||||
|
};
|
||||||
|
|
||||||
|
frequency = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "915MHz";
|
||||||
|
description = "Default frequency description displayed in the UI";
|
||||||
|
};
|
||||||
|
|
||||||
|
contactLink = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "#potatomesh:dod.ngo";
|
||||||
|
description = "Chat link or Matrix alias rendered in the footer and overlays";
|
||||||
|
};
|
||||||
|
|
||||||
|
mapCenter = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "38.761944,-27.090833";
|
||||||
|
description = "Latitude and longitude that centre the map on load";
|
||||||
|
};
|
||||||
|
|
||||||
|
mapZoom = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.int;
|
||||||
|
default = null;
|
||||||
|
description = "Fixed Leaflet zoom applied on first load; disables auto-fit when provided";
|
||||||
|
};
|
||||||
|
|
||||||
|
maxDistance = lib.mkOption {
|
||||||
|
type = lib.types.int;
|
||||||
|
default = 42;
|
||||||
|
description = "Maximum distance (km) before node relationships are hidden on the map";
|
||||||
|
};
|
||||||
|
|
||||||
|
debug = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Enable verbose logging";
|
||||||
|
};
|
||||||
|
|
||||||
|
allowedChannels = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
description = "Comma-separated channel names the ingestor accepts";
|
||||||
|
};
|
||||||
|
|
||||||
|
hiddenChannels = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
description = "Comma-separated channel names the ingestor will ignore";
|
||||||
|
};
|
||||||
|
|
||||||
|
federation = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Announce instance and crawl peers";
|
||||||
|
};
|
||||||
|
|
||||||
|
private = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Hide chat UI, disable message APIs, and exclude hidden clients from public listings";
|
||||||
|
};
|
||||||
|
|
||||||
|
dataDir = lib.mkOption {
|
||||||
|
type = lib.types.path;
|
||||||
|
default = "/var/lib/potato-mesh";
|
||||||
|
description = "Directory to store database and configuration";
|
||||||
|
};
|
||||||
|
|
||||||
|
user = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "potato-mesh";
|
||||||
|
description = "User to run the service as";
|
||||||
|
};
|
||||||
|
|
||||||
|
group = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "potato-mesh";
|
||||||
|
description = "Group to run the service as";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Ingestor options
|
||||||
|
ingestor = {
|
||||||
|
enable = lib.mkEnableOption "PotatoMesh Python ingestor";
|
||||||
|
|
||||||
|
package = lib.mkOption {
|
||||||
|
type = lib.types.package;
|
||||||
|
default = self.packages.${pkgs.system}.ingestor;
|
||||||
|
description = "The potato-mesh ingestor package to use";
|
||||||
|
};
|
||||||
|
|
||||||
|
connection = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "/dev/ttyACM0";
|
||||||
|
description = "Connection target: serial port, IP:port for TCP, or Bluetooth address for BLE";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
users.users.${cfg.user} = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = cfg.group;
|
||||||
|
home = cfg.dataDir;
|
||||||
|
createHome = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
users.groups.${cfg.group} = {};
|
||||||
|
|
||||||
|
systemd.services.potato-mesh-web = {
|
||||||
|
description = "PotatoMesh Web Dashboard";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
after = [ "network.target" ];
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
RACK_ENV = "production";
|
||||||
|
APP_ENV = "production";
|
||||||
|
PORT = toString cfg.port;
|
||||||
|
HOST = cfg.host;
|
||||||
|
SITE_NAME = cfg.siteName;
|
||||||
|
CHANNEL = cfg.channel;
|
||||||
|
FREQUENCY = cfg.frequency;
|
||||||
|
CONTACT_LINK = cfg.contactLink;
|
||||||
|
MAP_CENTER = cfg.mapCenter;
|
||||||
|
MAX_DISTANCE = toString cfg.maxDistance;
|
||||||
|
DEBUG = if cfg.debug then "1" else "0";
|
||||||
|
FEDERATION = if cfg.federation then "1" else "0";
|
||||||
|
PRIVATE = if cfg.private then "1" else "0";
|
||||||
|
XDG_DATA_HOME = cfg.dataDir;
|
||||||
|
XDG_CONFIG_HOME = "${cfg.dataDir}/config";
|
||||||
|
} // lib.optionalAttrs (cfg.instanceDomain != null) {
|
||||||
|
INSTANCE_DOMAIN = cfg.instanceDomain;
|
||||||
|
} // lib.optionalAttrs (cfg.mapZoom != null) {
|
||||||
|
MAP_ZOOM = toString cfg.mapZoom;
|
||||||
|
} // lib.optionalAttrs (cfg.allowedChannels != null) {
|
||||||
|
ALLOWED_CHANNELS = cfg.allowedChannels;
|
||||||
|
} // lib.optionalAttrs (cfg.hiddenChannels != null) {
|
||||||
|
HIDDEN_CHANNELS = cfg.hiddenChannels;
|
||||||
|
} // lib.optionalAttrs (cfg.apiToken != null) {
|
||||||
|
API_TOKEN = cfg.apiToken;
|
||||||
|
};
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "simple";
|
||||||
|
User = cfg.user;
|
||||||
|
Group = cfg.group;
|
||||||
|
WorkingDirectory = cfg.dataDir;
|
||||||
|
ExecStart = "${cfg.package}/bin/potato-mesh-web";
|
||||||
|
Restart = "always";
|
||||||
|
RestartSec = 5;
|
||||||
|
} // lib.optionalAttrs (cfg.apiTokenFile != null) {
|
||||||
|
EnvironmentFile = cfg.apiTokenFile;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.potato-mesh-ingestor = lib.mkIf cfg.ingestor.enable {
|
||||||
|
description = "PotatoMesh Python Ingestor";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
after = [ "network.target" "potato-mesh-web.service" ];
|
||||||
|
requires = [ "potato-mesh-web.service" ];
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
INSTANCE_DOMAIN = "http://127.0.0.1:${toString cfg.port}";
|
||||||
|
CONNECTION = cfg.ingestor.connection;
|
||||||
|
DEBUG = if cfg.debug then "1" else "0";
|
||||||
|
} // lib.optionalAttrs (cfg.allowedChannels != null) {
|
||||||
|
ALLOWED_CHANNELS = cfg.allowedChannels;
|
||||||
|
} // lib.optionalAttrs (cfg.hiddenChannels != null) {
|
||||||
|
HIDDEN_CHANNELS = cfg.hiddenChannels;
|
||||||
|
} // lib.optionalAttrs (cfg.apiToken != null) {
|
||||||
|
API_TOKEN = cfg.apiToken;
|
||||||
|
};
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "simple";
|
||||||
|
User = cfg.user;
|
||||||
|
Group = cfg.group;
|
||||||
|
WorkingDirectory = cfg.dataDir;
|
||||||
|
ExecStart = "${cfg.ingestor.package}/bin/potato-mesh-ingestor";
|
||||||
|
Restart = "always";
|
||||||
|
RestartSec = 10;
|
||||||
|
} // lib.optionalAttrs (cfg.apiTokenFile != null) {
|
||||||
|
EnvironmentFile = cfg.apiTokenFile;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user