Add service infrastructure for long-running deployment

- Add configuration system (config/*.exs, OrgGarden.Config)
- Refactor supervision tree with DynamicSupervisor and Registry
- Add OrgGarden.Server for serve mode lifecycle management
- Add health check HTTP endpoints (Bandit/Plug on :9090)
- Add telemetry events for export and watcher operations
- Implement graceful shutdown with SIGTERM handling
- Add Mix Release support with overlay scripts
- Add NixOS module for systemd service deployment
- Update documentation with service usage
This commit is contained in:
Ignacio Ballesteros
2026-02-21 20:38:47 +01:00
parent 6476b45f04
commit 01805dbf39
23 changed files with 1147 additions and 83 deletions

View File

@@ -48,6 +48,8 @@ defmodule OrgGarden.CLI do
require Logger
alias OrgGarden.Config
@transforms [OrgGarden.Transforms.Citations]
def main(argv) do
@@ -70,31 +72,36 @@ defmodule OrgGarden.CLI do
def handle_serve(argv) do
require_quartz_env()
{notes_dir, output_dir, content_dir, opts} = parse_serve_args(argv)
pipeline_opts = build_pipeline_opts()
# Initial batch export
wipe(content_dir)
export_all(notes_dir, output_dir)
run_pipeline(content_dir, pipeline_opts)
generate_index(content_dir)
IO.puts("==> Starting development server...")
{:ok, _pid} =
OrgGarden.Supervisor.start_link(
notes_dir: notes_dir,
output_dir: output_dir,
content_dir: content_dir,
pipeline_opts: pipeline_opts,
transforms: @transforms,
port: opts[:port] || 8080,
ws_port: opts[:ws_port] || 3001
)
case OrgGarden.Server.start_link(
notes_dir: notes_dir,
output_dir: output_dir,
content_dir: content_dir,
port: opts[:port],
ws_port: opts[:ws_port]
) do
{:ok, pid} ->
IO.puts("==> Server running at http://localhost:#{opts[:port] || Config.get(:http_port, 8080)}")
IO.puts("==> Watching #{notes_dir} for changes (Ctrl+C to stop)")
IO.puts("==> Server running at http://localhost:#{opts[:port] || 8080}")
IO.puts("==> Watching #{notes_dir} for changes (Ctrl+C to stop)")
# Wait for server to exit
ref = Process.monitor(pid)
Process.sleep(:infinity)
receive do
{:DOWN, ^ref, :process, ^pid, reason} ->
case reason do
:normal -> :ok
:shutdown -> :ok
{:shutdown, _} -> :ok
_ -> abort("Server crashed: #{inspect(reason)}")
end
end
{:error, reason} ->
abort("Failed to start server: #{inspect(reason)}")
end
end
defp parse_serve_args(argv) do
@@ -122,7 +129,7 @@ defmodule OrgGarden.CLI do
def handle_build(argv) do
quartz_path = require_quartz_env()
{notes_dir, output_dir, content_dir, _opts} = parse_build_args(argv)
pipeline_opts = build_pipeline_opts()
pipeline_opts = Config.pipeline_opts()
# Full batch export
wipe(content_dir)
@@ -130,7 +137,7 @@ defmodule OrgGarden.CLI do
run_pipeline(content_dir, pipeline_opts)
generate_index(content_dir)
node_path = System.get_env("NODE_PATH", "node")
node_path = Config.get(:node_path, "node")
IO.puts("==> Building static site with Quartz...")
@@ -177,7 +184,7 @@ defmodule OrgGarden.CLI do
def handle_export(argv) do
{notes_dir, output_dir, content_dir, watch?} = parse_export_args(argv)
pipeline_opts = build_pipeline_opts()
pipeline_opts = Config.pipeline_opts()
# Phase 1-4: full batch export
wipe(content_dir)
@@ -197,16 +204,29 @@ defmodule OrgGarden.CLI do
if watch? do
IO.puts("==> Watching #{notes_dir} for .org changes... (Ctrl+C to stop)")
{:ok, _pid} =
OrgGarden.Watcher.start_link(
notes_dir: notes_dir,
output_dir: output_dir,
content_dir: content_dir,
pipeline_opts: pipeline_opts,
transforms: @transforms
{:ok, pid} =
DynamicSupervisor.start_child(
OrgGarden.DynamicSupervisor,
{OrgGarden.Watcher,
notes_dir: notes_dir,
output_dir: output_dir,
content_dir: content_dir,
pipeline_opts: pipeline_opts,
transforms: @transforms}
)
Process.sleep(:infinity)
# Wait for watcher to exit
ref = Process.monitor(pid)
receive do
{:DOWN, ^ref, :process, ^pid, reason} ->
case reason do
:normal -> :ok
:shutdown -> :ok
{:shutdown, _} -> :ok
_ -> abort("Watcher crashed: #{inspect(reason)}")
end
end
end
end
@@ -235,7 +255,7 @@ defmodule OrgGarden.CLI do
dir
[] ->
System.get_env("NOTES_DIR") ||
Config.get(:notes_dir) ||
abort("Usage: org-garden #{command} <notes-dir> [options]")
end
@@ -249,7 +269,7 @@ defmodule OrgGarden.CLI do
end
defp extract_output_dir(opts) do
(opts[:output] || System.get_env("OUTPUT_DIR") || File.cwd!())
(opts[:output] || Config.get(:output_dir) || File.cwd!())
|> Path.expand()
end
@@ -328,7 +348,7 @@ defmodule OrgGarden.CLI do
# ---------------------------------------------------------------------------
defp require_quartz_env do
case System.get_env("QUARTZ_PATH") do
case Config.get(:quartz_path) do
nil ->
abort("""
Error: QUARTZ_PATH environment variable not set.
@@ -355,19 +375,6 @@ defmodule OrgGarden.CLI do
end
end
defp build_pipeline_opts do
%{
zotero_url: System.get_env("ZOTERO_URL", "http://localhost:23119"),
bibtex_file: System.get_env("BIBTEX_FILE"),
citation_mode:
case System.get_env("CITATION_MODE", "warn") do
"silent" -> :silent
"strict" -> :strict
_ -> :warn
end
}
end
defp abort(message) do
IO.puts(:stderr, message)
System.halt(1)