diff --git a/nixos/flake.lock b/nixos/flake.lock index c6af5db6..6d2b20e6 100644 --- a/nixos/flake.lock +++ b/nixos/flake.lock @@ -29,7 +29,7 @@ "railbird-secrets", "nixpkgs" ], - "systems": "systems_7" + "systems": "systems_9" }, "locked": { "lastModified": 1707830867, @@ -123,6 +123,28 @@ "type": "github" } }, + "emacs-overlay": { + "inputs": { + "nixpkgs": [ + "org-agenda-api", + "nixpkgs" + ], + "nixpkgs-stable": "nixpkgs-stable" + }, + "locked": { + "lastModified": 1769448145, + "narHash": "sha256-499V3+SiDzp+Vkwo2Osp1AkstlWeDc3KzfOn+xoiu6Y=", + "owner": "nix-community", + "repo": "emacs-overlay", + "rev": "2d2050b17743f564ffed290602e7816614a0dbdd", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "emacs-overlay", + "type": "github" + } + }, "flake-compat": { "flake": false, "locked": { @@ -341,6 +363,42 @@ "type": "github" } }, + "flake-utils_10": { + "inputs": { + "systems": "systems_13" + }, + "locked": { + "lastModified": 1685518550, + "narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_11": { + "inputs": { + "systems": "systems_14" + }, + "locked": { + "lastModified": 1681202837, + "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "cfacdce06f30d2b68473a46042957675eebb3401", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, "flake-utils_2": { "inputs": { "systems": "systems_3" @@ -414,9 +472,45 @@ } }, "flake-utils_6": { + "inputs": { + "systems": "systems_7" + }, + "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" + } + }, + "flake-utils_7": { "inputs": { "systems": "systems_8" }, + "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" + } + }, + "flake-utils_8": { + "inputs": { + "systems": "systems_10" + }, "locked": { "lastModified": 1709126324, "narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=", @@ -431,9 +525,9 @@ "type": "github" } }, - "flake-utils_7": { + "flake-utils_9": { "inputs": { - "systems": "systems_10" + "systems": "systems_12" }, "locked": { "lastModified": 1710146030, @@ -449,42 +543,6 @@ "type": "github" } }, - "flake-utils_8": { - "inputs": { - "systems": "systems_11" - }, - "locked": { - "lastModified": 1685518550, - "narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_9": { - "inputs": { - "systems": "systems_12" - }, - "locked": { - "lastModified": 1681202837, - "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "cfacdce06f30d2b68473a46042957675eebb3401", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, "fourmolu-011": { "flake": false, "locked": { @@ -625,6 +683,29 @@ "type": "github" } }, + "git-sync-rs": { + "inputs": { + "flake-utils": "flake-utils_7", + "nixpkgs": [ + "org-agenda-api", + "nixpkgs" + ], + "rust-overlay": "rust-overlay" + }, + "locked": { + "lastModified": 1769370203, + "narHash": "sha256-J52a10KE7GrR4EZjZiKdOOOeSe2uJiCYmUz1nVqC+Ec=", + "owner": "colonelpanic8", + "repo": "git-sync-rs", + "rev": "a0a68207460cc8ffed6b0f7a3045d5c4cae826c9", + "type": "github" + }, + "original": { + "owner": "colonelpanic8", + "repo": "git-sync-rs", + "type": "github" + } + }, "gitignore": { "inputs": { "nixpkgs": [ @@ -990,7 +1071,7 @@ "haskell-language-server_2": { "inputs": { "flake-compat": "flake-compat_6", - "flake-utils": "flake-utils_8", + "flake-utils": "flake-utils_10", "fourmolu-011": "fourmolu-011_2", "fourmolu-012": "fourmolu-012_2", "gitignore": "gitignore_4", @@ -1002,7 +1083,7 @@ "lsp": "lsp_2", "lsp-test": "lsp-test_2", "lsp-types": "lsp-types_2", - "nixpkgs": "nixpkgs_15", + "nixpkgs": "nixpkgs_16", "ormolu-052": "ormolu-052_2", "ormolu-07": "ormolu-07_2", "stylish-haskell-0145": "stylish-haskell-0145_2" @@ -1641,6 +1722,22 @@ "url": "https://hackage.haskell.org/package/lsp-2.2.0.0/lsp-2.2.0.0.tar.gz" } }, + "mova": { + "flake": false, + "locked": { + "lastModified": 1769543761, + "narHash": "sha256-z7MzEnGWJPeSeQ6gBw3Cj4frE7rGOTtd8BFVPABIlSw=", + "owner": "colonelpanic8", + "repo": "mova", + "rev": "d4c61ee8489d3bf9a0ff91c0a95171e939f8ba1b", + "type": "github" + }, + "original": { + "owner": "colonelpanic8", + "repo": "mova", + "type": "github" + } + }, "nix": { "inputs": { "flake-compat": "flake-compat_3", @@ -1784,6 +1881,22 @@ "type": "github" } }, + "nixpkgs-stable": { + "locked": { + "lastModified": 1767313136, + "narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.05", + "repo": "nixpkgs", + "type": "github" + } + }, "nixpkgs_10": { "locked": { "lastModified": 1769170682, @@ -1849,6 +1962,22 @@ } }, "nixpkgs_14": { + "locked": { + "lastModified": 1744536153, + "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_15": { "locked": { "lastModified": 1709703039, "narHash": "sha256-6hqgQ8OK6gsMu1VtcGKBxKQInRLHtzulDo9Z5jxHEFY=", @@ -1864,7 +1993,7 @@ "type": "github" } }, - "nixpkgs_15": { + "nixpkgs_16": { "locked": { "lastModified": 1686874404, "narHash": "sha256-u2Ss8z+sGaVlKtq7sCovQ8WvXY+OoXJmY1zmyxITiaY=", @@ -1880,7 +2009,7 @@ "type": "github" } }, - "nixpkgs_16": { + "nixpkgs_17": { "locked": { "lastModified": 1682134069, "narHash": "sha256-TnI/ZXSmRxQDt2sjRYK/8j8iha4B4zP2cnQCZZ3vp7k=", @@ -2063,6 +2192,81 @@ "type": "github" } }, + "org-agenda-api": { + "inputs": { + "emacs-overlay": "emacs-overlay", + "flake-utils": "flake-utils_6", + "git-sync-rs": "git-sync-rs", + "mova": "mova", + "nixpkgs": [ + "nixpkgs" + ], + "org-project-capture": "org-project-capture", + "org-wild-notifier": "org-wild-notifier", + "org-window-habit": "org-window-habit" + }, + "locked": { + "lastModified": 1769633802, + "narHash": "sha256-7rtHTFTqcQcXtOlBtZSAa0MAeUeDxkmS+xRYLmVxW2c=", + "owner": "colonelpanic8", + "repo": "org-agenda-api", + "rev": "2f05853be4c90f996f55c18b11535fea2a31d8d0", + "type": "github" + }, + "original": { + "owner": "colonelpanic8", + "repo": "org-agenda-api", + "type": "github" + } + }, + "org-project-capture": { + "flake": false, + "locked": { + "lastModified": 1768723645, + "narHash": "sha256-W/Y825QZ3WsUK+Kvio138W/6vS4hQ9xPqy2MZRpYfYM=", + "owner": "colonelpanic8", + "repo": "org-project-capture", + "rev": "4303dd869b0d638bd09014702803cde50f4e7fed", + "type": "github" + }, + "original": { + "owner": "colonelpanic8", + "repo": "org-project-capture", + "type": "github" + } + }, + "org-wild-notifier": { + "flake": false, + "locked": { + "lastModified": 1769492021, + "narHash": "sha256-1CWB61A9yAFZLcwylShXh7/cM5EDyTsBPZd/XOLAGEY=", + "owner": "emacsorphanage", + "repo": "org-wild-notifier", + "rev": "9211d14128a1097f78081dd7f8ba849671604575", + "type": "github" + }, + "original": { + "owner": "emacsorphanage", + "repo": "org-wild-notifier", + "type": "github" + } + }, + "org-window-habit": { + "flake": false, + "locked": { + "lastModified": 1769531910, + "narHash": "sha256-jiWxQW56z9yA7KJHxr70QIvWKTh+CVFi+9Biund2Xjg=", + "owner": "colonelpanic8", + "repo": "org-window-habit", + "rev": "39cdc616b5b8eb21716324e51ac7b2e9ccedb50f", + "type": "github" + }, + "original": { + "owner": "colonelpanic8", + "repo": "org-window-habit", + "type": "github" + } + }, "ormolu-052": { "flake": false, "locked": { @@ -2173,8 +2377,8 @@ "railbird-secrets": { "inputs": { "agenix": "agenix_2", - "flake-utils": "flake-utils_6", - "nixpkgs": "nixpkgs_14" + "flake-utils": "flake-utils_8", + "nixpkgs": "nixpkgs_15" }, "locked": { "lastModified": 1769209874, @@ -2208,15 +2412,34 @@ "nixpkgs": "nixpkgs_10", "nixtheplanet": "nixtheplanet", "notifications-tray-icon": "notifications-tray-icon", + "org-agenda-api": "org-agenda-api", "railbird-secrets": "railbird-secrets", "status-notifier-item": "status-notifier-item_2", - "systems": "systems_9", + "systems": "systems_11", "taffybar": "taffybar_2", "vscode-server": "vscode-server", "xmonad": "xmonad_3", "xmonad-contrib": "xmonad-contrib" } }, + "rust-overlay": { + "inputs": { + "nixpkgs": "nixpkgs_14" + }, + "locked": { + "lastModified": 1755311859, + "narHash": "sha256-NspGtm0ZpihxlFD628pvh5ZEhL/Q6/Z9XBpe3n6ZtEw=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "07619500e5937cc4669f24fec355d18a8fec0165", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, "status-notifier-item": { "flake": false, "locked": { @@ -2372,6 +2595,36 @@ "type": "github" } }, + "systems_13": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_14": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, "systems_2": { "locked": { "lastModified": 1689347949, @@ -2525,7 +2778,7 @@ }, "taffybar_2": { "inputs": { - "flake-utils": "flake-utils_7", + "flake-utils": "flake-utils_9", "git-ignore-nix": "git-ignore-nix_3", "gtk-sni-tray": "gtk-sni-tray_3", "gtk-strut": "gtk-strut_4", @@ -2601,8 +2854,8 @@ }, "vscode-server": { "inputs": { - "flake-utils": "flake-utils_9", - "nixpkgs": "nixpkgs_16" + "flake-utils": "flake-utils_11", + "nixpkgs": "nixpkgs_17" }, "locked": { "lastModified": 1753541826, diff --git a/nixos/flake.nix b/nixos/flake.nix index b86d8dde..66abf478 100644 --- a/nixos/flake.nix +++ b/nixos/flake.nix @@ -27,6 +27,11 @@ agenix = {url = "github:ryantm/agenix";}; + org-agenda-api = { + url = "github:colonelpanic8/org-agenda-api"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + # Hyprland and plugins from official flakes for proper plugin compatibility hyprland = { url = "git+https://github.com/hyprwm/Hyprland?submodules=1&ref=refs/tags/v0.53.0"; @@ -138,6 +143,8 @@ imalison-taffybar, hyprland, hy3, + org-agenda-api, + flake-utils, ... }: let # Nixpkgs PR patches - just specify PR number and hash @@ -315,10 +322,12 @@ extra-substituters = [ "http://192.168.1.26:5050" "https://cache.flox.dev" + "https://org-agenda-api.cachix.org" ]; extra-trusted-public-keys = [ "1896Folsom.duckdns.org:U2FTjvP95qwAJo0oGpvmUChJCgi5zQoG1YisoI08Qoo=" "flox-cache-public-1:7F4OyH7ZCnFhcze3fJdfyXYLQw/aV7GEed86nQ7IsOs=" + "org-agenda-api.cachix.org-1:liKFemKkOLV/rJt2txDNcpDjRsqLuBneBjkSw/UVXKA=" ]; }; nixosConfigurations = @@ -332,5 +341,57 @@ mkConfig (params // machineParams) ) defaultConfigurationParams; - }; + } // flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { inherit system; }; + + # Get short revs for tagging + orgApiRev = builtins.substring 0 7 (org-agenda-api.rev or "unknown"); + dotfilesRev = builtins.substring 0 7 (self.rev or self.dirtyRev or "dirty"); + + # Get tangled config files from org-agenda-api.nix + dotfilesOrgApi = import ./org-agenda-api.nix { + inherit pkgs system; + inherit inputs; + }; + tangledConfig = dotfilesOrgApi.org-agenda-custom-config; + + # Import container build logic + containerLib = import ../org-agenda-api/container.nix { + inherit pkgs system tangledConfig org-agenda-api orgApiRev dotfilesRev; + }; + in { + packages = { + container-colonelpanic = containerLib.containers.colonelpanic; + container-kat = containerLib.containers.kat; + # Default container + container = containerLib.containers.colonelpanic; + }; + + # Dev shell for org-agenda-api deployment + devShells.org-agenda-api = pkgs.mkShell { + buildInputs = [ + pkgs.flyctl + agenix.packages.${system}.default + pkgs.age + pkgs.ssh-to-age + pkgs.git + pkgs.jq + pkgs.just + pkgs.curl + ]; + shellHook = '' + echo "" + echo "org-agenda-api deployment shell" + echo "" + echo "Commands:" + echo " just --list - Show available API commands" + echo " ./deploy.sh - Deploy to Fly.io (colonelpanic or kat)" + echo " flyctl - Fly.io CLI" + echo " agenix -e - Edit encrypted secrets" + echo "" + ''; + }; + } + ); } diff --git a/org-agenda-api/.envrc b/org-agenda-api/.envrc new file mode 100644 index 00000000..221e0476 --- /dev/null +++ b/org-agenda-api/.envrc @@ -0,0 +1 @@ +use flake ../nixos#org-agenda-api diff --git a/org-agenda-api/.gitignore b/org-agenda-api/.gitignore new file mode 100644 index 00000000..e6a12f0e --- /dev/null +++ b/org-agenda-api/.gitignore @@ -0,0 +1,2 @@ +result* +.direnv/ diff --git a/org-agenda-api/configs/colonelpanic/config.env b/org-agenda-api/configs/colonelpanic/config.env new file mode 100644 index 00000000..813b03ea --- /dev/null +++ b/org-agenda-api/configs/colonelpanic/config.env @@ -0,0 +1,6 @@ +# colonelpanic instance configuration +FLY_APP="colonelpanic-org-agenda" +GIT_SYNC_REPOSITORY="git@github.com:colonelpanic8/org.git" +GIT_USER_EMAIL="org-agenda-api@colonelpanic.io" +GIT_USER_NAME="org-agenda-api" +AUTH_USER="imalison" diff --git a/org-agenda-api/configs/colonelpanic/custom-config.el b/org-agenda-api/configs/colonelpanic/custom-config.el new file mode 100644 index 00000000..944700c4 --- /dev/null +++ b/org-agenda-api/configs/colonelpanic/custom-config.el @@ -0,0 +1,252 @@ +;;; custom-config.el --- Container config loader -*- lexical-binding: t; -*- + +;; Helper function used by org-config (must be defined before loading preface) +(defun imalison:join-paths (&rest paths) + "Join PATHS together into a single path." + (let ((result (car paths))) + (dolist (p (cdr paths)) + (setq result (expand-file-name p result))) + result)) + +;; Load tangled config files in order +(let ((config-dir (file-name-directory load-file-name))) + ;; Load preface first (defines variables with default values) + (when (file-exists-p (expand-file-name "org-config-preface.el" config-dir)) + (load (expand-file-name "org-config-preface.el" config-dir))) + + ;; Override paths for container environment AFTER loading preface + ;; Use setq to ensure we override the defvar values from preface + (setq imalison:org-dir "/data/org") + (setq imalison:shared-org-dir nil) + + ;; Re-derive all path variables using the container org-dir + (setq imalison:org-gtd-file (imalison:join-paths imalison:org-dir "gtd.org")) + (setq imalison:org-habits-file (imalison:join-paths imalison:org-dir "habits.org")) + (setq imalison:org-calendar-file (imalison:join-paths imalison:org-dir "calendar.org")) + (setq imalison:org-inbox-file (imalison:join-paths imalison:org-dir "inbox.org")) + + ;; Shared paths are nil in container (no shared org dir) + (setq imalison:shared-org-gtd-file nil) + (setq imalison:shared-habits-file nil) + (setq imalison:shared-calendar-file nil) + (setq imalison:shared-shopping-file nil) + (setq imalison:shared-repeating-file nil) + (setq imalison:orgzly-files (list (imalison:join-paths imalison:org-dir "orgzly.org"))) + (setq imalison:repeating-org-files (list imalison:org-habits-file)) + + ;; Enable habits in agenda - this adds habits.org to org-agenda-files + (setq imalison:include-repeating-in-agenda t) + + ;; org-config-custom.el uses customize format (var value), convert to setq + (when (file-exists-p (expand-file-name "org-config-custom.el" config-dir)) + (with-temp-buffer + (insert-file-contents (expand-file-name "org-config-custom.el" config-dir)) + (goto-char (point-min)) + (condition-case nil + (while t + (let ((form (read (current-buffer)))) + (when (and (listp form) (symbolp (car form))) + (set (car form) (eval (cadr form)))))) + (end-of-file nil)))) + + ;; Load main config (sets up org-agenda-files using the variables we just set) + (when (file-exists-p (expand-file-name "org-config-config.el" config-dir)) + (load (expand-file-name "org-config-config.el" config-dir))) + + ;; Load optional overrides (instance-specific customizations) + (when (file-exists-p (expand-file-name "overrides.el" config-dir)) + (load (expand-file-name "overrides.el" config-dir)))) + +;; Define no-op stubs for unavailable packages (overwrite autoloads) +(defun org-bullets-mode (&optional _arg) + "No-op stub for org-bullets-mode (package not available in container)." + nil) + +;; Override shared-org-file-p to handle nil imalison:shared-org-dir +;; The original calls (file-truename imalison:shared-org-dir) which errors when nil +(defun imalison:shared-org-file-p () + "Check if current file is in the shared org directory. +Returns nil if imalison:shared-org-dir is not set." + (and imalison:shared-org-dir + (string-prefix-p (file-truename imalison:shared-org-dir) + (file-truename default-directory)))) + +;; Helper functions used by org-agenda-custom-commands +;; These are defined in README.org but needed for custom views +(defun imalison:compare-int-list (a b) + "Compare two lists of integers lexicographically." + (when (and a b) + (cond ((> (car a) (car b)) 1) + ((< (car a) (car b)) -1) + (t (imalison:compare-int-list (cdr a) (cdr b)))))) + +(defun get-date-created-from-agenda-entry (agenda-entry) + "Get the CREATED property timestamp from an agenda entry." + (org-time-string-to-time + (org-entry-get (get-text-property 1 'org-marker agenda-entry) "CREATED"))) + +;; Auto-convert org-capture-templates to org-agenda-api-capture-templates +(defun imalison:extract-template-string (template-spec) + "Extract the template string from TEMPLATE-SPEC. +Handles string templates, function templates, and file templates." + (let ((template-part (nth 4 template-spec))) + (cond + ((stringp template-part) template-part) + ((and (listp template-part) + (eq (car template-part) 'function)) + ;; Try to evaluate the function to get template string + (condition-case nil + (let ((result (funcall (eval (cadr template-part))))) + (if (stringp result) result "")) + (error ""))) + ((and (listp template-part) + (eq (car template-part) 'file)) + ;; File template - read file contents + (condition-case nil + (with-temp-buffer + (insert-file-contents (eval (cadr template-part))) + (buffer-string)) + (error ""))) + (t "")))) + +(defun imalison:infer-prompt-type (prompt-match) + "Infer the prompt type from PROMPT-MATCH. +PROMPT-MATCH is the full match string like \"%^{Name}t\" or \"%^{Title}\"." + (let ((suffix (substring prompt-match (1- (length prompt-match))))) + (cond + ((string-match-p "[tTuU]$" prompt-match) 'date) + ((string= "g" suffix) 'tag) + ((string= "G" suffix) 'tag) + ((string= "C" suffix) 'string) ; completion + (t 'string)))) + +(defun imalison:extract-prompts-from-template (template-string) + "Extract prompt definitions from TEMPLATE-STRING. +Returns a list of (NAME :type TYPE :required t) for each %^{...} found." + (let ((prompts '()) + (seen-names '()) + (pos 0)) + (while (string-match "%\\^{\\([^}|]+\\)\\(?:|[^}]*\\)?}\\([tTuUgGC]\\)?" template-string pos) + (let* ((name (match-string 1 template-string)) + (full-match (match-string 0 template-string)) + (type (imalison:infer-prompt-type full-match))) + (unless (member name seen-names) + (push name seen-names) + (push (list name :type type :required t) prompts)) + (setq pos (match-end 0)))) + (nreverse prompts))) + +(defun imalison:convert-capture-template (template-spec) + "Convert a single org-capture TEMPLATE-SPEC to API format. +Returns nil for non-entry templates or templates that can't be converted." + (when (and (listp template-spec) + (>= (length template-spec) 4)) + (let* ((key (nth 0 template-spec)) + (description (nth 1 template-spec)) + (type (nth 2 template-spec))) + ;; Only convert entry-type templates, skip menu items (no type) + (when (and (stringp key) + (stringp description) + (eq type 'entry)) + (let* ((template-string (imalison:extract-template-string template-spec)) + (prompts (imalison:extract-prompts-from-template template-string)) + ;; Create a unique API key from the hotkey + (api-key (concat "capture-" key))) + (list api-key + :name description + :template template-spec + :prompts prompts)))))) + +(defun imalison:convert-all-capture-templates () + "Convert all org-capture-templates to org-agenda-api-capture-templates format." + (let ((converted '())) + (dolist (template org-capture-templates) + (let ((api-template (imalison:convert-capture-template template))) + (when api-template + (push api-template converted)))) + (nreverse converted))) + +;; Default prompts for TODO templates when extraction fails +(defvar imalison:default-todo-prompts + '(("Title" :type string :required t)) + "Default prompts for TODO capture templates.") + +(defun imalison:ensure-prompts (template-plist) + "Ensure TEMPLATE-PLIST has non-empty prompts, using defaults if needed." + (let ((prompts (plist-get template-plist :prompts))) + (if (and prompts (not (null prompts))) + template-plist + (plist-put (copy-sequence template-plist) :prompts imalison:default-todo-prompts)))) + +;; Auto-generate API capture templates from org-capture-templates +;; Add a "default" entry that mirrors the "g" (GTD Todo) template +;; Remove "capture-g" from the list since "default" is its replacement +;; Ensure all templates have prompts (use defaults if extraction failed) +(let* ((converted (imalison:convert-all-capture-templates)) + ;; Ensure all converted templates have prompts + (converted-with-prompts + (mapcar (lambda (tmpl) + (cons (car tmpl) (imalison:ensure-prompts (cdr tmpl)))) + converted)) + (gtd-template (assoc "capture-g" converted-with-prompts)) + (filtered (if gtd-template + (cl-remove "capture-g" converted-with-prompts :key #'car :test #'string=) + converted-with-prompts)) + (default-entry (if gtd-template + (cons "default" (cdr gtd-template)) + (list "default" :name "GTD Todo" :prompts imalison:default-todo-prompts)))) + (setq org-agenda-api-capture-templates (cons default-entry filtered))) + +;; Set up org-project-capture for container environment +(require 'org-project-capture) +(require 'org-category-capture) + +(setq org-project-capture-projects-file + (imalison:join-paths imalison:org-dir "projects.org")) + +;; Create the org-project-capture strategy for the container +;; Use single-file strategy since we don't have projectile in the container +(setq org-project-capture-strategy + (make-instance 'org-project-capture-single-file-strategy)) + +(setq org-project-capture-capture-template + (format "%s%s" "* TODO %?" imalison:created-property-string)) + +;; Register category strategies for the API +;; This exposes org-project-capture categories through the /category-types, +;; /categories, /category-tasks, and /category-capture endpoints +(setq org-agenda-api-category-strategies + `(("projects" :strategy ,org-project-capture-strategy + :template ,org-project-capture-capture-template))) + +;; Add project capture todo files to org-agenda-files +(when (fboundp 'org-project-capture-todo-files) + (imalison:add-to-org-agenda-files (org-project-capture-todo-files))) + +;; Also add the main projects file +(imalison:add-to-org-agenda-files (list org-project-capture-projects-file)) + +;; Configure org-wild-notifier for the API +;; These settings must be set explicitly since we call org-wild-notifier functions +;; directly without enabling org-wild-notifier-mode +(when (require 'org-wild-notifier nil t) + (setq org-wild-notifier-keyword-whitelist nil) + (setq org-wild-notifier-tags-blacklist '("nonotify")) + (setq org-wild-notifier-alert-time '(10)) + (setq org-wild-notifier-show-any-overdue-with-day-wide-alerts t) + (setq org-wild-notifier-day-wide-alert-times '("10pm")) + (message "org-wild-notifier configured for API")) + +;; Enable org-window-habit if available in the container +;; This provides enhanced habit tracking with visual progress indicators +(when (require 'org-window-habit nil t) + (setq org-window-habit-property-prefix nil) + (setq org-window-habit-repeat-to-scheduled t) + (setq org-window-habit-preceding-intervals 21) + (setq org-window-habit-following-days 2) + (org-window-habit-mode +1) + (message "org-window-habit-mode enabled")) +;; The org-agenda-api-window-habit.el module is now included in the container, +;; providing /habit-config and /habit-status endpoints for window-habit integration. + +;;; custom-config.el ends here diff --git a/org-agenda-api/configs/colonelpanic/fly.toml b/org-agenda-api/configs/colonelpanic/fly.toml new file mode 100644 index 00000000..cabfe459 --- /dev/null +++ b/org-agenda-api/configs/colonelpanic/fly.toml @@ -0,0 +1,35 @@ +# fly.toml - Fly.io deployment configuration +# See https://fly.io/docs/reference/configuration/ + +app = "colonelpanic-org-agenda" +primary_region = "ord" + +# Image is built from flake and passed via deploy.sh --image flag + +[env] + TZ = "America/Los_Angeles" + +[http_service] + internal_port = 80 + force_https = true + auto_stop_machines = false + auto_start_machines = true + min_machines_running = 1 + + # Give services time to start (nginx + emacs) + [http_service.concurrency] + type = "connections" + hard_limit = 25 + soft_limit = 20 + +[[http_service.checks]] + grace_period = "120s" + interval = "30s" + timeout = "10s" + method = "GET" + path = "/health" + +[[vm]] + cpu_kind = "shared" + cpus = 1 + memory_mb = 512 diff --git a/org-agenda-api/configs/colonelpanic/secrets/auth-password.age b/org-agenda-api/configs/colonelpanic/secrets/auth-password.age new file mode 100644 index 00000000..8cd8d0b5 --- /dev/null +++ b/org-agenda-api/configs/colonelpanic/secrets/auth-password.age @@ -0,0 +1,40 @@ +age-encryption.org/v1 +-> ssh-rsa gwJx0Q +DPbChrJRIw/0GNKJFW0yVpHj/nS0De/I3t9z+tlqL9BFUpIKBNBZeLBW92xPdTgq +wAlDKj5Zl1qrxTga4DcHkzA7QJVhTg3GNnEeJqBwaToPb4yEyiToW8B05xjkSrMM +fPQ+ZkSbdTfupPrfjmnaHED/4RJXJFt2LvdI1dW4XSPdk4rp7oVbs3dnNqWObAhD +ATiQETPLJ33gAdyrM7A5xo7mwRBV+Kvjr+HrXX9dR3LhcMdecAVBbqI0508VjxvQ +XP2q30jfavX6x1cNuHNP9UbKRWFZrRvbxi2soz7V7wM5hSiJIuVuYS2Y0hGdIGJy +SdSozcsa2wJ/aobH4fMImg +-> ssh-ed25519 YFIoHA 0bHvbcnQZjfBkScK0vXJXjTVzAjPRiUz2wn3l1vQplI +2wmc1m1XT0f1nHRlgfAGvBJ1xFM84Y0/pVvTSPyxFOU +-> ssh-ed25519 KQfiow QFs5J3qngIVkFSae2SN1WWtHKzaNvWeaqw/I6Cukbys +k1YNLwNCKjZNUkwM7CwIEK8FcICPElPr94JEXM4bYrI +-> ssh-ed25519 kScIxg ucsEcywwdpkDxL327bgvegJXx7/tQf5DZkxl/82bxDY +BjaZ78vGeZz54/JK+on8TEaQpK7LHmuZ6OtAyszMjV4 +-> ssh-ed25519 HzX1zw VQtRquhDhEFTRBa0S0gVEJW90AeTb5hPe8bXJAyxyXw +rcUxBPvkIJUk3fvY6vGZgY+mCSzQLDFmKJ5tYafDvAc +-> ssh-ed25519 1o2X0w +6SwM5hLyc8wvbxBDJbfivjV+wN7whIgFRdX5z2LiCQ +a+UxJ2YKAWV+XJtOluMvq/8p0nfw27lF2JOzvCYHkmg +-> ssh-ed25519 KQ5iUA 8BSg4c4T4y0w3msKAmLZJY+J+oHw25mPPlKdFkj5Mlg +PsAz9C4XthB/hfn/F4IJ9Ifq280B9IXix0C1mk9RyI8 +-> ssh-ed25519 AKGkDw KcGz1q5Fe5RdnYKQtHZpuZtcUtdYysxxzGm10rSFvQ8 +/j2QuqhT1xQDjz/N6KGsBmEGIaL+Cm10YNHeZ6Sw+VU +-> ssh-ed25519 0eS5+A QLIh5xJ3+A9eeGMkvzbk01HNi41CaTjRGN75y00SSl0 +/Axjo9OeRpgTYxfugpiAeQTiTEtbUgUXWc1Rg7aggI0 +-> ssh-ed25519 9/4Prw 7jpwaON0KKlubIDN3/xllVX7pZhJRoaMVnX5Sc7+wkw +ObueU1r7o5F9D0e/DxZkU40AsN9lXK5eOcF0N2M1H0g +-> ssh-ed25519 gAk3+Q cklYccOHJ2HFT7m3Oje8McxLif8kOma0h36ERo2uEC8 +2pPIQfO+V45jdFEZCFRUUFGY1aZWindpoPbCLE1Mies +-> ssh-ed25519 X6eGtQ IOpU7iT9w8Dmehx/LEA1Cpr8BnAIFwl2sOj8ZZiY00I +dfx6A+tL7Xc4PXRdt6zxh2rtrB3Wb5HhAhpHT+n9cx0 +-> ssh-ed25519 0ma8Cw C0hjuEmVLn+djtVvJURuVi8b47JVEcux84P6QoX/fGs +Vnf3b3kU5zFyW9Km+idxgIlx+CFusnKBdN5sOsB/hVk +-> ssh-ed25519 Tp0Z1Q x4jysmX0AOaOWc0hiTzBA2Lwjwza5G/cqfcP22NuiC8 +pC3MxtgfZHQ1sk/JLtsBKUXPkkC53vH49OVwrWypq1A +-> ssh-ed25519 ePNWZQ QPNC4Hw4cLAgZgso+Vgqz60sBd1wUgOVUqxl7yYMEkQ +99w0DohDiy6fXwbKHZYcFZNSvCUroxBxerHVPKY16lg +-> ssh-ed25519 hILzzA 8tShLcIvpPifSyY0OjKH2fj2F0rgHAol0LRSAAE+7Vw +9K3iKvI+KKhjY75rWt3n0v9Bz4yqP548PTgWi35c9m4 +--- foeVPG32rt4SIuJ0BtwWh+mTUVoQipAapftZUIA/7gw +=QuE Uu-WT3YLiu@?'G2}E USAYi \ No newline at end of file diff --git a/org-agenda-api/configs/colonelpanic/secrets/git-ssh-key.age b/org-agenda-api/configs/colonelpanic/secrets/git-ssh-key.age new file mode 100644 index 00000000..db1bb07a Binary files /dev/null and b/org-agenda-api/configs/colonelpanic/secrets/git-ssh-key.age differ diff --git a/org-agenda-api/configs/kat/config.env b/org-agenda-api/configs/kat/config.env new file mode 100644 index 00000000..00b1fc33 --- /dev/null +++ b/org-agenda-api/configs/kat/config.env @@ -0,0 +1,6 @@ +# kat instance configuration +FLY_APP="kat-org-agenda-api" +GIT_SYNC_REPOSITORIES='[{"url":"ssh://gitea@dev.railbird.ai:1123/colonelpanic/katnivan.git","path":"org"},{"url":"ssh://gitea@dev.railbird.ai:1123/kkathuang/org.git","path":"shared"}]' +GIT_USER_EMAIL="kat-org-agenda-api@colonelpanic.io" +GIT_USER_NAME="kat-org-agenda-api" +AUTH_USER="kat" diff --git a/org-agenda-api/configs/kat/custom-config.el b/org-agenda-api/configs/kat/custom-config.el new file mode 100644 index 00000000..988af46a --- /dev/null +++ b/org-agenda-api/configs/kat/custom-config.el @@ -0,0 +1,171 @@ +;;; custom-config.el --- Container config loader -*- lexical-binding: t; -*- + +;; Helper function used by org-config (must be defined before loading preface) +(defun imalison:join-paths (&rest paths) + "Join PATHS together into a single path." + (let ((result (car paths))) + (dolist (p (cdr paths)) + (setq result (expand-file-name p result))) + result)) + +;; Load tangled config files in order +(let ((config-dir (file-name-directory load-file-name))) + ;; Load preface first (defines variables with default values) + (when (file-exists-p (expand-file-name "org-config-preface.el" config-dir)) + (load (expand-file-name "org-config-preface.el" config-dir))) + + ;; Override paths for container environment AFTER loading preface + ;; Use setq to ensure we override the defvar values from preface + (setq imalison:org-dir "/data/org") + (setq imalison:shared-org-dir "/data/shared") + + ;; Re-derive all path variables using the container org-dir + (setq imalison:org-gtd-file (imalison:join-paths imalison:org-dir "gtd.org")) + (setq imalison:org-habits-file (imalison:join-paths imalison:org-dir "habits.org")) + (setq imalison:org-calendar-file (imalison:join-paths imalison:org-dir "calendar.org")) + (setq imalison:org-inbox-file (imalison:join-paths imalison:org-dir "inbox.org")) + + ;; Shared paths derived from shared-org-dir + (setq imalison:shared-org-gtd-file (imalison:join-paths imalison:shared-org-dir "gtd.org")) + (setq imalison:shared-habits-file (imalison:join-paths imalison:shared-org-dir "habits.org")) + (setq imalison:shared-calendar-file (imalison:join-paths imalison:shared-org-dir "calendar.org")) + (setq imalison:shared-shopping-file (imalison:join-paths imalison:shared-org-dir "shopping.org")) + (setq imalison:shared-repeating-file (imalison:join-paths imalison:shared-org-dir "repeating.org")) + (setq imalison:orgzly-files (list (imalison:join-paths imalison:org-dir "orgzly.org"))) + (setq imalison:repeating-org-files (list imalison:org-habits-file + imalison:shared-habits-file + imalison:shared-repeating-file)) + + ;; org-config-custom.el uses customize format (var value), convert to setq + (when (file-exists-p (expand-file-name "org-config-custom.el" config-dir)) + (with-temp-buffer + (insert-file-contents (expand-file-name "org-config-custom.el" config-dir)) + (goto-char (point-min)) + (condition-case nil + (while t + (let ((form (read (current-buffer)))) + (when (and (listp form) (symbolp (car form))) + (set (car form) (eval (cadr form)))))) + (end-of-file nil)))) + + ;; Load main config (sets up org-agenda-files using the variables we just set) + (when (file-exists-p (expand-file-name "org-config-config.el" config-dir)) + (load (expand-file-name "org-config-config.el" config-dir))) + + ;; Load optional overrides (instance-specific customizations) + (when (file-exists-p (expand-file-name "overrides.el" config-dir)) + (load (expand-file-name "overrides.el" config-dir)))) + +;; Define no-op stubs for unavailable packages (overwrite autoloads) +(defun org-bullets-mode (&optional _arg) + "No-op stub for org-bullets-mode (package not available in container)." + nil) + +;; Override shared-org-file-p to handle nil imalison:shared-org-dir +;; The original calls (file-truename imalison:shared-org-dir) which errors when nil +(defun imalison:shared-org-file-p () + "Check if current file is in the shared org directory. +Returns nil if imalison:shared-org-dir is not set." + (and imalison:shared-org-dir + (string-prefix-p (file-truename imalison:shared-org-dir) + (file-truename default-directory)))) + +;; Helper functions used by org-agenda-custom-commands +;; These are defined in README.org but needed for custom views +(defun imalison:compare-int-list (a b) + "Compare two lists of integers lexicographically." + (when (and a b) + (cond ((> (car a) (car b)) 1) + ((< (car a) (car b)) -1) + (t (imalison:compare-int-list (cdr a) (cdr b)))))) + +(defun get-date-created-from-agenda-entry (agenda-entry) + "Get the CREATED property timestamp from an agenda entry." + (org-time-string-to-time + (org-entry-get (get-text-property 1 'org-marker agenda-entry) "CREATED"))) + +;; Auto-convert org-capture-templates to org-agenda-api-capture-templates +(defun imalison:extract-template-string (template-spec) + "Extract the template string from TEMPLATE-SPEC. +Handles string templates, function templates, and file templates." + (let ((template-part (nth 4 template-spec))) + (cond + ((stringp template-part) template-part) + ((and (listp template-part) + (eq (car template-part) 'function)) + ;; Try to evaluate the function to get template string + (condition-case nil + (let ((result (funcall (eval (cadr template-part))))) + (if (stringp result) result "")) + (error ""))) + ((and (listp template-part) + (eq (car template-part) 'file)) + ;; File template - read file contents + (condition-case nil + (with-temp-buffer + (insert-file-contents (eval (cadr template-part))) + (buffer-string)) + (error ""))) + (t "")))) + +(defun imalison:infer-prompt-type (prompt-match) + "Infer the prompt type from PROMPT-MATCH. +PROMPT-MATCH is the full match string like \"%^{Name}t\" or \"%^{Title}\"." + (let ((suffix (substring prompt-match (1- (length prompt-match))))) + (cond + ((string-match-p "[tTuU]$" prompt-match) 'date) + ((string= "g" suffix) 'tag) + ((string= "G" suffix) 'tag) + ((string= "C" suffix) 'string) ; completion + (t 'string)))) + +(defun imalison:extract-prompts-from-template (template-string) + "Extract prompt definitions from TEMPLATE-STRING. +Returns a list of (NAME :type TYPE :required t) for each %^{...} found." + (let ((prompts '()) + (seen-names '()) + (pos 0)) + (while (string-match "%\\^{\\([^}|]+\\)\\(?:|[^}]*\\)?}\\([tTuUgGC]\\)?" template-string pos) + (let* ((name (match-string 1 template-string)) + (full-match (match-string 0 template-string)) + (type (imalison:infer-prompt-type full-match))) + (unless (member name seen-names) + (push name seen-names) + (push (list name :type type :required t) prompts)) + (setq pos (match-end 0)))) + (nreverse prompts))) + +(defun imalison:convert-capture-template (template-spec) + "Convert a single org-capture TEMPLATE-SPEC to API format. +Returns nil for non-entry templates or templates that can't be converted." + (when (and (listp template-spec) + (>= (length template-spec) 4)) + (let* ((key (nth 0 template-spec)) + (description (nth 1 template-spec)) + (type (nth 2 template-spec))) + ;; Only convert entry-type templates, skip menu items (no type) + (when (and (stringp key) + (stringp description) + (eq type 'entry)) + (let* ((template-string (imalison:extract-template-string template-spec)) + (prompts (imalison:extract-prompts-from-template template-string)) + ;; Create a unique API key from the hotkey + (api-key (concat "capture-" key))) + (list api-key + :name description + :template template-spec + :prompts prompts)))))) + +(defun imalison:convert-all-capture-templates () + "Convert all org-capture-templates to org-agenda-api-capture-templates format." + (let ((converted '())) + (dolist (template org-capture-templates) + (let ((api-template (imalison:convert-capture-template template))) + (when api-template + (push api-template converted)))) + (nreverse converted))) + +;; Auto-generate API capture templates from org-capture-templates +(setq org-agenda-api-capture-templates (imalison:convert-all-capture-templates)) + +;;; custom-config.el ends here diff --git a/org-agenda-api/configs/kat/fly.toml b/org-agenda-api/configs/kat/fly.toml new file mode 100644 index 00000000..833390bb --- /dev/null +++ b/org-agenda-api/configs/kat/fly.toml @@ -0,0 +1,32 @@ +# fly.toml - Fly.io deployment configuration +# See https://fly.io/docs/reference/configuration/ + +app = "kat-org-agenda-api" +primary_region = "ord" + +# Image is built from flake and passed via deploy.sh --image flag + +[http_service] + internal_port = 80 + force_https = true + auto_stop_machines = false + auto_start_machines = true + min_machines_running = 1 + + # Give services time to start (nginx + emacs) + [http_service.concurrency] + type = "connections" + hard_limit = 25 + soft_limit = 20 + +[[http_service.checks]] + grace_period = "10s" + interval = "30s" + timeout = "5s" + method = "GET" + path = "/health" + +[[vm]] + cpu_kind = "shared" + cpus = 1 + memory_mb = 512 diff --git a/org-agenda-api/configs/kat/secrets/auth-password.age b/org-agenda-api/configs/kat/secrets/auth-password.age new file mode 100644 index 00000000..ba848aa0 --- /dev/null +++ b/org-agenda-api/configs/kat/secrets/auth-password.age @@ -0,0 +1,5 @@ +age-encryption.org/v1 +-> ssh-ed25519 Tp0Z1Q ZvS3ewY5ZCm7rWQeliPHPXnzSpfFeqK/1a7pWY/l83s +gJoiP/tSEsPYrxiVFsD1eIRPALL2tdKJFWBNMj/dpAk +--- lN9hXwr2IFAJjhe/u52xiOpGTaDU/fWXhhquOhgBc8U +<}t] 0@y՝z?Y~R4?"޵D9Xp}C \ No newline at end of file diff --git a/org-agenda-api/configs/kat/secrets/git-ssh-key.age b/org-agenda-api/configs/kat/secrets/git-ssh-key.age new file mode 100644 index 00000000..490a0d64 --- /dev/null +++ b/org-agenda-api/configs/kat/secrets/git-ssh-key.age @@ -0,0 +1,42 @@ +age-encryption.org/v1 +-> ssh-rsa gwJx0Q +ugmhcDkQfVGmgY2b4IY0DT9ORMInX+qa8HSaes5RmNItUeRP3ufIeXvSVKC+R0zV +YCqOf2uyLyjUIOupRKw/1kTh5Jr+74tX2ug4C8dD2A132yDpyziuA1AoKP3P5Uhp +M1pBA9/QtIwzZByA7gYd5aU7SIqXHR/+DM/dnt/uuKXqKfvDPJrCZWcXIbf2k7xP +Tyd6NBWfbVmUFZi23qLzN9ugl51Wq7VOvvIpzh+trKFmeTuMyvMCjhRGr/m50sGs +y3HRMuoXnkTAKXRZ4aQpHGhTwnVgS3SJWHzhH6OVn2ReSObOKZHieYl55kqcJc99 +0ONaWAy54klsa9uHTO4C6w +-> ssh-ed25519 YFIoHA a6Mdqeh2kYAKiD5etrZiFIlXZaeFcaMooabCWFyG1D0 +zkHFO8d7/I7qnLchwv3ONNqaxqwenX6o/0ky8Z2A+W0 +-> ssh-ed25519 KQfiow vuC5xZ6WYVrpqv44zyNAqFqKlA221SPdpBSm4mlpSU0 +9yXSgczQxZviQf4ZpsAwKa+JOxWH/MQsU5dq4+94Xyc +-> ssh-ed25519 kScIxg Jy61wrM+pU7QzEIHO9WRJ3KDNKfFk1ekylqRSgzKwVc +h3ZHKf+cFeGVjgXwof9KlLyDtx43vaSafzgrOiQXe5I +-> ssh-ed25519 HzX1zw 1en9ihLijagyKd0c7QMJYtH96oIX2phAhJQqM4ysBi4 +JOBDfrom9qo1ZUn99jcxva3DP58yWOIFzwQebJx/158 +-> ssh-ed25519 KQfiow WCcqJwJoqy0dnOb/aUe8oW4BGmkZ4PwEijFJEI1o1ho +WLjp7UxiY5Kfwq+z5JQzapxUhncZTfsjMUi7FdMtpTU +-> ssh-ed25519 1o2X0w aOnNDcvUxNjHT9nKDWvlOBPg2nFUHiweT8oz6vvdF2M +L/PXsInIR+iN/2GStkkL7dnJR1aekne8vfgHi3wADck +-> ssh-ed25519 KQ5iUA PNAoXHVxtqbzzBfyfdPm4zVBi/ck3Pu5AQpGVqXpQyE +C98+eL5rcKXgI41iikybPrKC3Zr+uFMTyQOWczOulhs +-> ssh-ed25519 AKGkDw lDwdoiwK/TKbxDORSggj7wFqoQMguUrzWuqpMZz2Y0U +rezIK003vf5Jqr7yMED2A6AzIuywDK4TZDYOAGxr5WI +-> ssh-ed25519 0eS5+A btXiZqnbH6Q0uYNAeu95W6X1h2j8a8MDJNSRYPGYyg0 +0/wsL9Va6+db9BEJ/TJEg6gLo7HbdzLpPKdgUiTqoMc +-> ssh-ed25519 9/4Prw RMEfjY5nsrEAKFn4IyLD3OyzNVmxgVG3tfNJ4CTbryA +7PaRcNos01e83BrxgjR9C1FizAlqY+pq+er5lVwVn0w +-> ssh-ed25519 gAk3+Q ktstz8AM5OafJekAf3SDdIi3VJXudT7q3dWx2hUSWWE +Yyu/Vudc6aXQXyCQ6rRvFFer5d8IeHegNnpb19UODfs +-> ssh-ed25519 X6eGtQ nGjC6TfQPXxDnbDVlR8OC6IID2a8uHMkBsft9AvoDzE +zGue+yzKNJTTOEsRMf1KGTxqjRAEfqII40t5MTMqoYU +-> ssh-ed25519 0ma8Cw dvgCcSrhVazWrmhkOd60NKLB5nZliQNpdiDYQCDYaAE +6GtVprnzy25LtPYtqynoZ+Jy4SI1Y8h0jToAueURptg +-> ssh-ed25519 Tp0Z1Q c8MDIlbXAWC9IWMJMg7HvOBdd4ujm67TyyhjBt2lcio +2sRBX77p6C+4ZZsiZRLSkyIffVoJL4LFfMFiDMKhvxg +-> ssh-ed25519 ePNWZQ Uz1bLbcA/U0aU0BCY/VR8HQU5G1CKg+duSkyXVHbk2k +1vyIWbDlw9YotcHhZ0rIVTaBiiUSsqFs4QVsMfTlreY +-> ssh-ed25519 hILzzA RWNiC+y7F1XzHMuiM7EQymDH2kyxUhKhRw7og3/frwI +MjcexeBdoWG0Sskj86zD5M9aRCT8cJcMkbiF0osDmV0 +--- yE8GNNA+dIpXZeFOX9zJYI39UPeTN3hYaaLsdOWk6PI +{P_'_#LWd󂫥NWQ} ૆( @M kH&szkiZ. ve>܅ͦι3OX}87|\Ou] 9KR3q5K-&fY 9wliFⲕ=:OUOJQ}6hE++=3^Xg|g\{\WaGF nݞ[sR^2[čRa@ yNtʀa[fK픎$ZWβ{1ʗ1'#[ICUтm]z[NFOA;k3U]),DH[H<bw3G6Nrc>ZMJӧu \ No newline at end of file diff --git a/org-agenda-api/configs/kat/secrets/git-ssh-key.pub b/org-agenda-api/configs/kat/secrets/git-ssh-key.pub new file mode 100644 index 00000000..60fa0ee1 --- /dev/null +++ b/org-agenda-api/configs/kat/secrets/git-ssh-key.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGZZsY0ho0kF6TXKEyDG316awWyrO05auloywf+NTiyb kat-org-agenda-api diff --git a/org-agenda-api/container.nix b/org-agenda-api/container.nix new file mode 100644 index 00000000..b330a3fc --- /dev/null +++ b/org-agenda-api/container.nix @@ -0,0 +1,43 @@ +# Container build logic for org-agenda-api instances +# Imported by the main dotfiles flake to expose container outputs +{ pkgs, system, tangledConfig, org-agenda-api, orgApiRev, dotfilesRev }: + +let + # Build container for a specific instance + mkInstanceContainer = instanceName: customConfigEl: overridesEl: + let + # Combine tangled config with instance-specific loader and overrides + orgAgendaCustomConfig = pkgs.runCommand "org-agenda-custom-config-${instanceName}" {} '' + mkdir -p $out + + # Copy tangled files from dotfiles + cp ${tangledConfig}/*.el $out/ 2>/dev/null || true + + # Add instance-specific custom-config.el loader + cp ${customConfigEl} $out/custom-config.el + + # Add optional overrides.el if provided + ${if overridesEl != null then "cp ${overridesEl} $out/overrides.el" else ""} + ''; + in + org-agenda-api.lib.${system}.mkContainer { + customElispFile = "${orgAgendaCustomConfig}/custom-config.el"; + # Use content-based tag to avoid caching issues + tag = "${instanceName}-${orgApiRev}-${dotfilesRev}"; + }; + + configsDir = ./configs; +in +{ + inherit mkInstanceContainer; + + # Pre-built instance containers + containers = { + colonelpanic = mkInstanceContainer "colonelpanic" + "${configsDir}/colonelpanic/custom-config.el" + null; + kat = mkInstanceContainer "kat" + "${configsDir}/kat/custom-config.el" + null; + }; +} diff --git a/org-agenda-api/deploy.sh b/org-agenda-api/deploy.sh new file mode 100755 index 00000000..989aab82 --- /dev/null +++ b/org-agenda-api/deploy.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Deploy customized org-agenda-api container to Fly.io +# Usage: ./deploy.sh [flyctl deploy args...] +# Example: ./deploy.sh colonelpanic +# ./deploy.sh kat + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +NIXOS_DIR="$SCRIPT_DIR/../nixos" +cd "$SCRIPT_DIR" + +# Parse instance argument +INSTANCE="${1:-}" +if [[ -z "$INSTANCE" ]]; then + echo "Usage: $0 [flyctl deploy args...]" + echo "Available instances:" + for dir in configs/*/; do + echo " - $(basename "$dir")" + done + exit 1 +fi +shift + +CONFIG_DIR="$SCRIPT_DIR/configs/$INSTANCE" +if [[ ! -d "$CONFIG_DIR" ]]; then + echo "Error: Instance '$INSTANCE' not found in configs/" + exit 1 +fi + +# Source instance configuration +if [[ -f "$CONFIG_DIR/config.env" ]]; then + source "$CONFIG_DIR/config.env" +else + echo "Error: $CONFIG_DIR/config.env not found" + exit 1 +fi + +echo "Deploying instance: $INSTANCE" +echo " Fly app: $FLY_APP" + +# Check for uncommitted changes +if [[ -n "$(git status --porcelain)" ]]; then + echo "" + echo "WARNING: Working directory has uncommitted changes!" + echo "For reproducibility, consider committing before deploying." + echo "" + read -p "Continue anyway? [y/N] " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi +fi + +# Get input revisions for reproducibility from the nixos flake +ORG_API_NODE=$(jq -r '.nodes.root.inputs."org-agenda-api"' "$NIXOS_DIR/flake.lock") +ORG_API_REV=$(jq -r ".nodes.\"$ORG_API_NODE\".locked.rev" "$NIXOS_DIR/flake.lock") +# For dotfiles rev, use the current git commit since we're building from local +DOTFILES_REV=$(git -C "$NIXOS_DIR/.." rev-parse HEAD) + +SHORT_DOTFILES="${DOTFILES_REV:0:7}" +SHORT_ORG_API="${ORG_API_REV:0:7}" + +echo "Versions:" +echo " org-agenda-api: $ORG_API_REV" +echo " dotfiles: $DOTFILES_REV" + +# Build container from nixos flake for this instance +# Use --refresh to ensure we're not using stale cached builds +echo "Building container from flake..." +nix build "$NIXOS_DIR#container-$INSTANCE" -o "result-container-$INSTANCE" --refresh + +# Load into Docker +echo "Loading container into Docker..." +LOADED_IMAGE=$(docker load < "result-container-$INSTANCE" 2>&1 | grep -oP 'Loaded image: \K.*') +echo "Loaded: $LOADED_IMAGE" + +# Tag with both versions for full reproducibility +# Format: api--cfg- +IMAGE_TAG="api-${SHORT_ORG_API}-cfg-${SHORT_DOTFILES}" +IMAGE_NAME="registry.fly.io/$FLY_APP:$IMAGE_TAG" +echo "Tagging as $IMAGE_NAME..." +docker tag "$LOADED_IMAGE" "$IMAGE_NAME" + +echo "Pushing to Fly.io registry..." +flyctl auth docker +docker push "$IMAGE_NAME" + +# Decrypt secrets +echo "Decrypting secrets..." + +IDENTITY="" +for key_type in ed25519 rsa; do + if [[ -f "$HOME/.ssh/id_${key_type}" ]]; then + IDENTITY="$HOME/.ssh/id_${key_type}" + break + fi +done + +if [[ -z "$IDENTITY" ]]; then + echo "Error: No SSH identity found" >&2 + exit 1 +fi + +GIT_SSH_KEY=$(age -d -i "$IDENTITY" "$CONFIG_DIR/secrets/git-ssh-key.age") +AUTH_PASSWORD=$(age -d -i "$IDENTITY" "$CONFIG_DIR/secrets/auth-password.age") + +echo "Setting Fly.io secrets..." + +SECRET_ARGS=( + "GIT_SSH_PRIVATE_KEY=$GIT_SSH_KEY" + "AUTH_USER=$AUTH_USER" + "AUTH_PASSWORD=$AUTH_PASSWORD" + "GIT_USER_EMAIL=$GIT_USER_EMAIL" + "GIT_USER_NAME=$GIT_USER_NAME" +) + +# Use GIT_SYNC_REPOSITORIES (multi-repo) or GIT_SYNC_REPOSITORY (single repo) +if [[ -n "${GIT_SYNC_REPOSITORIES:-}" ]]; then + SECRET_ARGS+=("GIT_SYNC_REPOSITORIES=$GIT_SYNC_REPOSITORIES") +elif [[ -n "${GIT_SYNC_REPOSITORY:-}" ]]; then + SECRET_ARGS+=("GIT_SYNC_REPOSITORY=$GIT_SYNC_REPOSITORY") +else + echo "Error: Neither GIT_SYNC_REPOSITORIES nor GIT_SYNC_REPOSITORY set in config.env" + exit 1 +fi + +flyctl secrets set "${SECRET_ARGS[@]}" --stage -a "$FLY_APP" + +echo "Deploying $IMAGE_NAME..." +flyctl deploy --image "$IMAGE_NAME" -c "$CONFIG_DIR/fly.toml" "$@" + +# Cleanup +rm -f "result-container-$INSTANCE" + +echo "" +echo "Done! Deployed to $FLY_APP" +echo " Image: $IMAGE_NAME" +echo " org-agenda-api: $ORG_API_REV" +echo " dotfiles: $DOTFILES_REV" diff --git a/org-agenda-api/justfile b/org-agenda-api/justfile new file mode 100644 index 00000000..711ca31b --- /dev/null +++ b/org-agenda-api/justfile @@ -0,0 +1,35 @@ +# org-agenda-api commands + +base_url := "https://colonelpanic-org-agenda.fly.dev" +user := "imalison" + +# Get all todos +get-all-todos: + @curl -s -u "{{user}}:$(pass show org-agenda-api/imalison | head -1)" "{{base_url}}/get-all-todos" | jq . + +# Get today's agenda +get-todays-agenda: + @curl -s -u "{{user}}:$(pass show org-agenda-api/imalison | head -1)" "{{base_url}}/get-todays-agenda" | jq . + +# Get agenda (day view) +agenda: + @curl -s -u "{{user}}:$(pass show org-agenda-api/imalison | head -1)" "{{base_url}}/agenda" | jq . + +# Get agenda files +agenda-files: + @curl -s -u "{{user}}:$(pass show org-agenda-api/imalison | head -1)" "{{base_url}}/agenda-files" | jq . + +# Get todo states +todo-states: + @curl -s -u "{{user}}:$(pass show org-agenda-api/imalison | head -1)" "{{base_url}}/todo-states" | jq . + +# Health check +health: + @curl -s "{{base_url}}/health" | jq . + +# Create a todo +create-todo title: + @curl -s -X POST -u "{{user}}:$(pass show org-agenda-api/imalison | head -1)" \ + -H "Content-Type: application/json" \ + -d '{"title": "{{title}}"}' \ + "{{base_url}}/create-todo" | jq . diff --git a/org-agenda-api/secrets.nix b/org-agenda-api/secrets.nix new file mode 100644 index 00000000..3c47a8e6 --- /dev/null +++ b/org-agenda-api/secrets.nix @@ -0,0 +1,12 @@ +let + keys = import ../nixos/keys.nix; +in +{ + # colonelpanic instance + "configs/colonelpanic/secrets/git-ssh-key.age".publicKeys = keys.kanivanKeys; + "configs/colonelpanic/secrets/auth-password.age".publicKeys = keys.kanivanKeys; + + # kat instance + "configs/kat/secrets/git-ssh-key.age".publicKeys = keys.kanivanKeys; + "configs/kat/secrets/auth-password.age".publicKeys = keys.kanivanKeys; +}