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

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