Add org-agenda-api container builds and fly.io deployment

Consolidates container builds from colonelpanic-org-agenda-api repo:
- Add org-agenda-api input to nixos flake
- Add container-colonelpanic and container-kat package outputs
- Add org-agenda-api cachix as substituter
- Add org-agenda-api devShell for deployment work

New org-agenda-api directory contains:
- container.nix: Container build logic using mkContainer
- configs/: Instance configs (custom-config.el, fly.toml, secrets)
- deploy.sh: Fly.io deployment script
- secrets.nix: agenix secret declarations

Build with: nix build .#container-colonelpanic
Deploy with: cd org-agenda-api && ./deploy.sh colonelpanic

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-28 14:24:41 -08:00
parent ccd63ba066
commit 504ec1a105
19 changed files with 1187 additions and 50 deletions

351
nixos/flake.lock generated
View File

@@ -29,7 +29,7 @@
"railbird-secrets", "railbird-secrets",
"nixpkgs" "nixpkgs"
], ],
"systems": "systems_7" "systems": "systems_9"
}, },
"locked": { "locked": {
"lastModified": 1707830867, "lastModified": 1707830867,
@@ -123,6 +123,28 @@
"type": "github" "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-compat": {
"flake": false, "flake": false,
"locked": { "locked": {
@@ -341,6 +363,42 @@
"type": "github" "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": { "flake-utils_2": {
"inputs": { "inputs": {
"systems": "systems_3" "systems": "systems_3"
@@ -414,9 +472,45 @@
} }
}, },
"flake-utils_6": { "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": { "inputs": {
"systems": "systems_8" "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": { "locked": {
"lastModified": 1709126324, "lastModified": 1709126324,
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=", "narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
@@ -431,9 +525,9 @@
"type": "github" "type": "github"
} }
}, },
"flake-utils_7": { "flake-utils_9": {
"inputs": { "inputs": {
"systems": "systems_10" "systems": "systems_12"
}, },
"locked": { "locked": {
"lastModified": 1710146030, "lastModified": 1710146030,
@@ -449,42 +543,6 @@
"type": "github" "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": { "fourmolu-011": {
"flake": false, "flake": false,
"locked": { "locked": {
@@ -625,6 +683,29 @@
"type": "github" "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": { "gitignore": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@@ -990,7 +1071,7 @@
"haskell-language-server_2": { "haskell-language-server_2": {
"inputs": { "inputs": {
"flake-compat": "flake-compat_6", "flake-compat": "flake-compat_6",
"flake-utils": "flake-utils_8", "flake-utils": "flake-utils_10",
"fourmolu-011": "fourmolu-011_2", "fourmolu-011": "fourmolu-011_2",
"fourmolu-012": "fourmolu-012_2", "fourmolu-012": "fourmolu-012_2",
"gitignore": "gitignore_4", "gitignore": "gitignore_4",
@@ -1002,7 +1083,7 @@
"lsp": "lsp_2", "lsp": "lsp_2",
"lsp-test": "lsp-test_2", "lsp-test": "lsp-test_2",
"lsp-types": "lsp-types_2", "lsp-types": "lsp-types_2",
"nixpkgs": "nixpkgs_15", "nixpkgs": "nixpkgs_16",
"ormolu-052": "ormolu-052_2", "ormolu-052": "ormolu-052_2",
"ormolu-07": "ormolu-07_2", "ormolu-07": "ormolu-07_2",
"stylish-haskell-0145": "stylish-haskell-0145_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" "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": { "nix": {
"inputs": { "inputs": {
"flake-compat": "flake-compat_3", "flake-compat": "flake-compat_3",
@@ -1784,6 +1881,22 @@
"type": "github" "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": { "nixpkgs_10": {
"locked": { "locked": {
"lastModified": 1769170682, "lastModified": 1769170682,
@@ -1849,6 +1962,22 @@
} }
}, },
"nixpkgs_14": { "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": { "locked": {
"lastModified": 1709703039, "lastModified": 1709703039,
"narHash": "sha256-6hqgQ8OK6gsMu1VtcGKBxKQInRLHtzulDo9Z5jxHEFY=", "narHash": "sha256-6hqgQ8OK6gsMu1VtcGKBxKQInRLHtzulDo9Z5jxHEFY=",
@@ -1864,7 +1993,7 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs_15": { "nixpkgs_16": {
"locked": { "locked": {
"lastModified": 1686874404, "lastModified": 1686874404,
"narHash": "sha256-u2Ss8z+sGaVlKtq7sCovQ8WvXY+OoXJmY1zmyxITiaY=", "narHash": "sha256-u2Ss8z+sGaVlKtq7sCovQ8WvXY+OoXJmY1zmyxITiaY=",
@@ -1880,7 +2009,7 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs_16": { "nixpkgs_17": {
"locked": { "locked": {
"lastModified": 1682134069, "lastModified": 1682134069,
"narHash": "sha256-TnI/ZXSmRxQDt2sjRYK/8j8iha4B4zP2cnQCZZ3vp7k=", "narHash": "sha256-TnI/ZXSmRxQDt2sjRYK/8j8iha4B4zP2cnQCZZ3vp7k=",
@@ -2063,6 +2192,81 @@
"type": "github" "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": { "ormolu-052": {
"flake": false, "flake": false,
"locked": { "locked": {
@@ -2173,8 +2377,8 @@
"railbird-secrets": { "railbird-secrets": {
"inputs": { "inputs": {
"agenix": "agenix_2", "agenix": "agenix_2",
"flake-utils": "flake-utils_6", "flake-utils": "flake-utils_8",
"nixpkgs": "nixpkgs_14" "nixpkgs": "nixpkgs_15"
}, },
"locked": { "locked": {
"lastModified": 1769209874, "lastModified": 1769209874,
@@ -2208,15 +2412,34 @@
"nixpkgs": "nixpkgs_10", "nixpkgs": "nixpkgs_10",
"nixtheplanet": "nixtheplanet", "nixtheplanet": "nixtheplanet",
"notifications-tray-icon": "notifications-tray-icon", "notifications-tray-icon": "notifications-tray-icon",
"org-agenda-api": "org-agenda-api",
"railbird-secrets": "railbird-secrets", "railbird-secrets": "railbird-secrets",
"status-notifier-item": "status-notifier-item_2", "status-notifier-item": "status-notifier-item_2",
"systems": "systems_9", "systems": "systems_11",
"taffybar": "taffybar_2", "taffybar": "taffybar_2",
"vscode-server": "vscode-server", "vscode-server": "vscode-server",
"xmonad": "xmonad_3", "xmonad": "xmonad_3",
"xmonad-contrib": "xmonad-contrib" "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": { "status-notifier-item": {
"flake": false, "flake": false,
"locked": { "locked": {
@@ -2372,6 +2595,36 @@
"type": "github" "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": { "systems_2": {
"locked": { "locked": {
"lastModified": 1689347949, "lastModified": 1689347949,
@@ -2525,7 +2778,7 @@
}, },
"taffybar_2": { "taffybar_2": {
"inputs": { "inputs": {
"flake-utils": "flake-utils_7", "flake-utils": "flake-utils_9",
"git-ignore-nix": "git-ignore-nix_3", "git-ignore-nix": "git-ignore-nix_3",
"gtk-sni-tray": "gtk-sni-tray_3", "gtk-sni-tray": "gtk-sni-tray_3",
"gtk-strut": "gtk-strut_4", "gtk-strut": "gtk-strut_4",
@@ -2601,8 +2854,8 @@
}, },
"vscode-server": { "vscode-server": {
"inputs": { "inputs": {
"flake-utils": "flake-utils_9", "flake-utils": "flake-utils_11",
"nixpkgs": "nixpkgs_16" "nixpkgs": "nixpkgs_17"
}, },
"locked": { "locked": {
"lastModified": 1753541826, "lastModified": 1753541826,

View File

@@ -27,6 +27,11 @@
agenix = {url = "github:ryantm/agenix";}; 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 and plugins from official flakes for proper plugin compatibility
hyprland = { hyprland = {
url = "git+https://github.com/hyprwm/Hyprland?submodules=1&ref=refs/tags/v0.53.0"; url = "git+https://github.com/hyprwm/Hyprland?submodules=1&ref=refs/tags/v0.53.0";
@@ -138,6 +143,8 @@
imalison-taffybar, imalison-taffybar,
hyprland, hyprland,
hy3, hy3,
org-agenda-api,
flake-utils,
... ...
}: let }: let
# Nixpkgs PR patches - just specify PR number and hash # Nixpkgs PR patches - just specify PR number and hash
@@ -315,10 +322,12 @@
extra-substituters = [ extra-substituters = [
"http://192.168.1.26:5050" "http://192.168.1.26:5050"
"https://cache.flox.dev" "https://cache.flox.dev"
"https://org-agenda-api.cachix.org"
]; ];
extra-trusted-public-keys = [ extra-trusted-public-keys = [
"1896Folsom.duckdns.org:U2FTjvP95qwAJo0oGpvmUChJCgi5zQoG1YisoI08Qoo=" "1896Folsom.duckdns.org:U2FTjvP95qwAJo0oGpvmUChJCgi5zQoG1YisoI08Qoo="
"flox-cache-public-1:7F4OyH7ZCnFhcze3fJdfyXYLQw/aV7GEed86nQ7IsOs=" "flox-cache-public-1:7F4OyH7ZCnFhcze3fJdfyXYLQw/aV7GEed86nQ7IsOs="
"org-agenda-api.cachix.org-1:liKFemKkOLV/rJt2txDNcpDjRsqLuBneBjkSw/UVXKA="
]; ];
}; };
nixosConfigurations = nixosConfigurations =
@@ -332,5 +341,57 @@
mkConfig (params // machineParams) mkConfig (params // machineParams)
) )
defaultConfigurationParams; 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 <instance> - Deploy to Fly.io (colonelpanic or kat)"
echo " flyctl - Fly.io CLI"
echo " agenix -e <file> - Edit encrypted secrets"
echo ""
'';
};
}
);
} }

1
org-agenda-api/.envrc Normal file
View File

@@ -0,0 +1 @@
use flake ../nixos#org-agenda-api

2
org-agenda-api/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
result*
.direnv/

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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
=<3D><><EFBFBD>Q<EFBFBD>uE <09><>U<EFBFBD>u<EFBFBD>-WT<57>3YL<1F><03>iu<69>@<40>?<3F><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>'<01>G2}<7D>E <20>US<>A<EFBFBD><41>Yi<59><01>

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,5 @@
age-encryption.org/v1
-> ssh-ed25519 Tp0Z1Q ZvS3ewY5ZCm7rWQeliPHPXnzSpfFeqK/1a7pWY/l83s
gJoiP/tSEsPYrxiVFsD1eIRPALL2tdKJFWBNMj/dpAk
--- lN9hXwr2IFAJjhe/u52xiOpGTaDU/fWXhhquOhgBc8U
<}t<><74><EFBFBD>]<5D> <20>0@<40>y՝z?Y~R4?"<22><>޵<0F>D9<44>X<EFBFBD><58>p}C<>

View File

@@ -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<><50><1B>_<EFBFBD>'<27><>_<>#<23><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><1F><03>L<EFBFBD><4C><14>W<>d󂫥NWQ<57>}<7D> <0E><><EFBFBD><EFBFBD><EFBFBD><E0AB86>( <0C> @<0F><>M<EFBFBD><4D><EFBFBD> <0B>k<><6B>H<EFBFBD>&<26>s<><73>z<05><>k<6B>i<EFBFBD><69>Z.

View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGZZsY0ho0kF6TXKEyDG316awWyrO05auloywf+NTiyb kat-org-agenda-api

View File

@@ -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;
};
}

140
org-agenda-api/deploy.sh Executable file
View File

@@ -0,0 +1,140 @@
#!/usr/bin/env bash
set -euo pipefail
# Deploy customized org-agenda-api container to Fly.io
# Usage: ./deploy.sh <instance> [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 <instance> [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-<org-api-rev>-cfg-<dotfiles-rev>
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"

35
org-agenda-api/justfile Normal file
View File

@@ -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 .

View File

@@ -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;
}