@celilo/e2e 0.7.11 → 0.7.13-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/.celilo-website-cache/_astro/docs.DwsUauWZ.css +1 -0
  2. package/.celilo-website-cache/_astro/network.BB0RR2mM.css +1 -0
  3. package/.celilo-website-cache/diagrams/physical.svg +1 -0
  4. package/.celilo-website-cache/diagrams/zone-topology.svg +1 -0
  5. package/.celilo-website-cache/docs/index.html +250 -0
  6. package/.celilo-website-cache/docs/network/index.html +300 -0
  7. package/.celilo-website-cache/favicon.svg +4 -0
  8. package/.celilo-website-cache/fonts/theia-300.otf +0 -0
  9. package/.celilo-website-cache/fonts/theia-400.otf +0 -0
  10. package/.celilo-website-cache/fonts/theia-500.otf +0 -0
  11. package/.celilo-website-cache/index.html +93 -0
  12. package/.celilo-website-cache/install.sh +330 -0
  13. package/.celilo-website-cache/modules/index.html +86 -0
  14. package/.celilo-website-cache/modules/view/index.html +69 -0
  15. package/.celilo-website-cache/vision/index.html +121 -0
  16. package/.npm-registry-cache/celilo-capabilities-0.1.10.tgz +0 -0
  17. package/.npm-registry-cache/celilo-cli-0.3.30.tgz +0 -0
  18. package/.npm-registry-cache/celilo-cli-display-0.1.9.tgz +0 -0
  19. package/.npm-registry-cache/celilo-e2e-0.7.13.tgz +0 -0
  20. package/.npm-registry-cache/celilo-event-bus-0.1.4.tgz +0 -0
  21. package/netapps/authentik.netapp +0 -0
  22. package/netapps/caddy.netapp +0 -0
  23. package/netapps/celilo-website.netapp +0 -0
  24. package/netapps/greenwave.netapp +0 -0
  25. package/netapps/homebridge.netapp +0 -0
  26. package/netapps/iptables.netapp +0 -0
  27. package/netapps/knot-unbound-internal.netapp +0 -0
  28. package/netapps/namecheap.netapp +0 -0
  29. package/netapps/technitium.netapp +0 -0
  30. package/package.json +6 -3
  31. package/src/docker-compose-generator.ts +30 -2
@@ -0,0 +1,330 @@
1
+ #!/usr/bin/env bash
2
+ # Celilo installer — https://celilo.computer
3
+ #
4
+ # Usage:
5
+ # curl -fsSL https://celilo.computer/install.sh | bash
6
+ #
7
+ # What it does (non-interactive — safe to run from CI / cloud-init):
8
+ # 1. Installs Bun (https://bun.sh) if it's missing
9
+ # 2. Installs @celilo/cli, @celilo/event-bus, @celilo/e2e as global Bun packages
10
+ # 3. Prints next steps (PATH, `celilo system init`, optional event-bus daemon)
11
+ #
12
+ # It does NOT run `celilo system init` (interactive — needs you at the keyboard).
13
+ # It does NOT install the event-bus daemon. Both are listed in the next-steps
14
+ # block at the end so you can run them when you're ready.
15
+
16
+ set -euo pipefail
17
+
18
+ # ---------- options ----------
19
+ # Lenient by design: this is the user's first contact with Celilo, and a
20
+ # hard-exit on a stale flag (e.g. someone copy-pastes an old curl-pipe from
21
+ # the docs that still has --skip-init) is a worse first impression than
22
+ # warning and continuing. We only recognize -h/--help; anything else gets
23
+ # a soft warning. If we add behavior-changing flags later, document the
24
+ # tradeoff at that time.
25
+ for arg in "$@"; do
26
+ case "$arg" in
27
+ -h|--help)
28
+ cat <<'EOF'
29
+ Celilo installer — https://celilo.computer
30
+
31
+ Usage:
32
+ curl -fsSL https://celilo.computer/install.sh | bash
33
+
34
+ The installer is non-interactive. It installs Bun, the Celilo CLI, the event
35
+ bus library, and the e2e harness, then prints next steps.
36
+
37
+ Options:
38
+ -h, --help Show this help
39
+ EOF
40
+ exit 0
41
+ ;;
42
+ *) printf 'warning: ignoring unknown option: %s\n' "$arg" >&2 ;;
43
+ esac
44
+ done
45
+
46
+ # ---------- output helpers ----------
47
+ if [ -t 1 ]; then
48
+ RED=$'\033[31m'; GREEN=$'\033[32m'; YELLOW=$'\033[33m'
49
+ BLUE=$'\033[34m'; BOLD=$'\033[1m'; RESET=$'\033[0m'
50
+ else
51
+ RED=""; GREEN=""; YELLOW=""; BLUE=""; BOLD=""; RESET=""
52
+ fi
53
+
54
+ info() { printf '%s[celilo]%s %s\n' "$BLUE" "$RESET" "$*"; }
55
+ ok() { printf '%s[celilo]%s %s\n' "$GREEN" "$RESET" "$*"; }
56
+ warn() { printf '%s[celilo]%s %s\n' "$YELLOW" "$RESET" "$*"; }
57
+ fail() { printf '%s[celilo]%s %s\n' "$RED" "$RESET" "$*" >&2; exit 1; }
58
+
59
+ # ---------- preflight ----------
60
+ case "$(uname -s)" in
61
+ Darwin|Linux) ;;
62
+ *) fail "Unsupported OS: $(uname -s). Celilo runs on macOS and Linux." ;;
63
+ esac
64
+
65
+ if ! command -v curl >/dev/null 2>&1; then
66
+ fail "Missing required command: curl. Install it and re-run."
67
+ fi
68
+
69
+ # ---------- 1. Bun ----------
70
+ export BUN_INSTALL="${BUN_INSTALL:-$HOME/.bun}"
71
+ export PATH="$BUN_INSTALL/bin:$PATH"
72
+
73
+ if ! command -v bun >/dev/null 2>&1; then
74
+ # Bun's installer ships its release as a .zip, so we need unzip — but only
75
+ # when Bun is actually missing. If Bun is already installed, no unzip needed.
76
+ if ! command -v unzip >/dev/null 2>&1; then
77
+ case "$(uname -s)" in
78
+ Linux)
79
+ if command -v apt-get >/dev/null 2>&1; then hint="sudo apt-get install unzip"
80
+ elif command -v dnf >/dev/null 2>&1; then hint="sudo dnf install unzip"
81
+ elif command -v yum >/dev/null 2>&1; then hint="sudo yum install unzip"
82
+ elif command -v pacman >/dev/null 2>&1; then hint="sudo pacman -S unzip"
83
+ elif command -v apk >/dev/null 2>&1; then hint="sudo apk add unzip"
84
+ else hint="install 'unzip' via your package manager"
85
+ fi
86
+ ;;
87
+ Darwin) hint="brew install unzip (or use macOS's bundled unzip from /usr/bin)" ;;
88
+ *) hint="install 'unzip' via your package manager" ;;
89
+ esac
90
+ fail "Missing 'unzip' (needed to install Bun). Try: $hint"
91
+ fi
92
+ info "Bun not found — installing from https://bun.sh"
93
+ curl -fsSL https://bun.sh/install | bash
94
+ if ! command -v bun >/dev/null 2>&1; then
95
+ fail "Bun install completed but 'bun' is still not on PATH. Open a new shell, ensure $BUN_INSTALL/bin is on \$PATH, and re-run."
96
+ fi
97
+ ok "Bun installed: $(bun --version)"
98
+ else
99
+ ok "Bun already installed: $(bun --version)"
100
+ fi
101
+
102
+ # ---------- 2. Celilo packages ----------
103
+ info "Installing @celilo/cli, @celilo/event-bus, @celilo/e2e (global)…"
104
+ bun add -g @celilo/cli @celilo/event-bus @celilo/e2e
105
+
106
+ if ! command -v celilo >/dev/null 2>&1; then
107
+ fail "Celilo install finished, but 'celilo' is not on PATH. Add $BUN_INSTALL/bin to PATH and re-run."
108
+ fi
109
+
110
+ ok "Celilo CLI installed: $(celilo --version 2>/dev/null || echo '<version check failed>')"
111
+
112
+ # ---------- 3. host prereqs (advisory) ----------
113
+ # Celilo shells out to ansible, ansible-galaxy, terraform, ssh, and git during
114
+ # `system init` and module deploy. They're not bundled with the CLI, and a
115
+ # missing prereq surfaces as a confusing "command not found" mid-deploy. Detect
116
+ # what's missing now and surface a per-OS install hint in the next-steps block.
117
+
118
+ have_apt=0; have_dnf=0; have_yum=0; have_pacman=0; have_apk=0; have_brew=0
119
+ case "$(uname -s)" in
120
+ Linux)
121
+ command -v apt-get >/dev/null 2>&1 && have_apt=1
122
+ command -v dnf >/dev/null 2>&1 && have_dnf=1
123
+ command -v yum >/dev/null 2>&1 && have_yum=1
124
+ command -v pacman >/dev/null 2>&1 && have_pacman=1
125
+ command -v apk >/dev/null 2>&1 && have_apk=1
126
+ ;;
127
+ Darwin)
128
+ command -v brew >/dev/null 2>&1 && have_brew=1
129
+ ;;
130
+ esac
131
+
132
+ # pm_install <pkg> — emit the install command for the detected package manager.
133
+ # Most prereqs share the same package name across pms; tools that don't
134
+ # (terraform) get a bespoke hint inline below.
135
+ pm_install() {
136
+ local pkg="$1"
137
+ if [ "$have_apt" = 1 ]; then printf 'sudo apt-get install -y %s' "$pkg"
138
+ elif [ "$have_dnf" = 1 ]; then printf 'sudo dnf install -y %s' "$pkg"
139
+ elif [ "$have_yum" = 1 ]; then printf 'sudo yum install -y %s' "$pkg"
140
+ elif [ "$have_pacman" = 1 ]; then printf 'sudo pacman -S --noconfirm %s' "$pkg"
141
+ elif [ "$have_apk" = 1 ]; then printf 'sudo apk add %s' "$pkg"
142
+ elif [ "$have_brew" = 1 ]; then printf 'brew install %s' "$pkg"
143
+ else printf 'install %s via your package manager' "$pkg"
144
+ fi
145
+ }
146
+
147
+ # Each entry: "<label>|<install command>"
148
+ PREREQ_HINTS=()
149
+
150
+ if ! command -v ansible >/dev/null 2>&1 || ! command -v ansible-galaxy >/dev/null 2>&1; then
151
+ PREREQ_HINTS+=("Ansible (runs playbooks)|$(pm_install ansible)")
152
+ fi
153
+
154
+ if ! command -v terraform >/dev/null 2>&1; then
155
+ case "$(uname -s)" in
156
+ Linux)
157
+ if [ "$have_apt" = 1 ]; then
158
+ tf_hint='wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg && echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list && sudo apt update && sudo apt install -y terraform'
159
+ elif [ "$have_dnf" = 1 ] || [ "$have_yum" = 1 ]; then
160
+ tf_hint='see https://developer.hashicorp.com/terraform/install#linux for the HashiCorp dnf/yum repo'
161
+ elif [ "$have_pacman" = 1 ]; then
162
+ tf_hint='sudo pacman -S terraform'
163
+ else
164
+ tf_hint='see https://developer.hashicorp.com/terraform/install'
165
+ fi
166
+ ;;
167
+ Darwin) tf_hint='brew tap hashicorp/tap && brew install hashicorp/tap/terraform' ;;
168
+ *) tf_hint='see https://developer.hashicorp.com/terraform/install' ;;
169
+ esac
170
+ PREREQ_HINTS+=("Terraform (provisions infrastructure)|$tf_hint")
171
+ fi
172
+
173
+ if ! command -v git >/dev/null 2>&1; then
174
+ PREREQ_HINTS+=("git (clones module sources)|$(pm_install git)")
175
+ fi
176
+
177
+ if ! command -v ssh >/dev/null 2>&1; then
178
+ case "$(uname -s)" in
179
+ Linux) ssh_hint=$(pm_install openssh-client) ;;
180
+ Darwin) ssh_hint='ssh ships with macOS — if it is missing your system is unusual; reinstall via xcode-select --install' ;;
181
+ *) ssh_hint='install OpenSSH client via your package manager' ;;
182
+ esac
183
+ PREREQ_HINTS+=("OpenSSH client (deploys to remote hosts)|$ssh_hint")
184
+ fi
185
+
186
+ # ---------- 4. shell completions ----------
187
+ # Write completion scripts for whichever shells are installed. We don't touch
188
+ # the user's rc files — Rule of Least Surprise — but we surface what they need
189
+ # to add (and a "reload your shell" reminder) in the next-steps block.
190
+ #
191
+ # Two parallel arrays drive the next-steps block:
192
+ # COMPLETIONS_INSTALLED — shells where a file was written successfully.
193
+ # Always triggers an "exec $SHELL to activate" hint.
194
+ # COMPLETION_RC_SNIPPETS — "<shell>|<rc-line>|<rc-target>" entries shown
195
+ # as conditional fallbacks ("if completion does not
196
+ # work, append this to your rc"). zsh always needs
197
+ # an fpath line; bash only needs a manual `source`
198
+ # when bash-completion v2 isn't auto-loading.
199
+ COMPLETIONS_INSTALLED=()
200
+ COMPLETION_RC_SNIPPETS=()
201
+
202
+ write_completion() {
203
+ local shell="$1" target="$2"
204
+ local target_dir
205
+ target_dir="$(dirname "$target")"
206
+ if ! mkdir -p "$target_dir" 2>/dev/null; then
207
+ return 1
208
+ fi
209
+ if ! celilo completion "$shell" >"$target" 2>/dev/null; then
210
+ rm -f "$target" 2>/dev/null
211
+ return 1
212
+ fi
213
+ return 0
214
+ }
215
+
216
+ if command -v zsh >/dev/null 2>&1; then
217
+ ZSH_COMP="$HOME/.zsh/completions/_celilo"
218
+ if write_completion zsh "$ZSH_COMP"; then
219
+ ok "Wrote zsh completion to $ZSH_COMP"
220
+ COMPLETIONS_INSTALLED+=("zsh")
221
+ # Always surface the fpath/compinit snippet — the file alone is useless
222
+ # without zsh knowing to scan ~/.zsh/completions and run compinit. We
223
+ # frame it as "if completion does not work" so users with an existing
224
+ # custom fpath setup don't think they need to duplicate it.
225
+ COMPLETION_RC_SNIPPETS+=("zsh|fpath=(\"\$HOME/.zsh/completions\" \$fpath)
226
+ autoload -Uz compinit && compinit|$HOME/.zshrc")
227
+ else
228
+ warn "Could not write zsh completion to $ZSH_COMP — install manually with: celilo completion zsh > $ZSH_COMP"
229
+ fi
230
+ fi
231
+
232
+ if command -v bash >/dev/null 2>&1; then
233
+ BASH_COMP="$HOME/.local/share/bash-completion/completions/celilo"
234
+ if write_completion bash "$BASH_COMP"; then
235
+ ok "Wrote bash completion to $BASH_COMP"
236
+ COMPLETIONS_INSTALLED+=("bash")
237
+ # bash-completion v2 (~/.local/share/bash-completion/completions auto-load)
238
+ # requires the bash-completion package to be installed AND the user's shell
239
+ # to source it from /etc/bash.bashrc or similar. We can't reliably detect
240
+ # the second part from a non-interactive shell, so always include the
241
+ # source fallback under "if completion does not work".
242
+ COMPLETION_RC_SNIPPETS+=("bash|source $BASH_COMP|$HOME/.bashrc")
243
+ else
244
+ warn "Could not write bash completion to $BASH_COMP — install manually with: celilo completion bash > $BASH_COMP"
245
+ fi
246
+ fi
247
+
248
+ if command -v fish >/dev/null 2>&1; then
249
+ FISH_COMP="$HOME/.config/fish/completions/celilo.fish"
250
+ if write_completion fish "$FISH_COMP"; then
251
+ ok "Wrote fish completion to $FISH_COMP"
252
+ COMPLETIONS_INSTALLED+=("fish")
253
+ # Fish auto-loads from ~/.config/fish/completions/ — no rc snippet needed.
254
+ else
255
+ warn "Could not write fish completion to $FISH_COMP — install manually with: celilo completion fish > $FISH_COMP"
256
+ fi
257
+ fi
258
+
259
+ # ---------- 6. PATH advisory ----------
260
+ RC_FILE=""
261
+ case "${SHELL:-}" in
262
+ */zsh) RC_FILE="$HOME/.zshrc" ;;
263
+ */bash)
264
+ if [ -f "$HOME/.bashrc" ]; then RC_FILE="$HOME/.bashrc"
265
+ elif [ -f "$HOME/.bash_profile" ]; then RC_FILE="$HOME/.bash_profile"
266
+ fi
267
+ ;;
268
+ */fish) RC_FILE="$HOME/.config/fish/config.fish" ;;
269
+ esac
270
+
271
+ PATH_LINE='export PATH="$HOME/.bun/bin:$PATH"'
272
+ PATH_NEEDS_RC=0
273
+ if [ -n "$RC_FILE" ] && [ -f "$RC_FILE" ] && grep -qs '\.bun/bin' "$RC_FILE"; then
274
+ : # already set up by Bun's installer — nothing to do
275
+ else
276
+ PATH_NEEDS_RC=1
277
+ fi
278
+
279
+ # ---------- 7. summary / next steps ----------
280
+ echo
281
+ ok "Celilo is installed."
282
+ echo
283
+ echo "Next steps:"
284
+ STEP=1
285
+ if [ "$PATH_NEEDS_RC" = "1" ]; then
286
+ RC_TARGET="${RC_FILE:-your shell rc}"
287
+ printf " %d. Add Bun to your PATH so 'celilo' works in new shells:\n" "$STEP"
288
+ printf " echo %s >> %s\n" "'$PATH_LINE'" "$RC_TARGET"
289
+ printf " exec \$SHELL\n"
290
+ STEP=$((STEP + 1))
291
+ fi
292
+ if [ "${#PREREQ_HINTS[@]}" -gt 0 ]; then
293
+ printf ' %d. Install host prerequisites (Celilo shells out to these during deploy):\n' "$STEP"
294
+ for entry in "${PREREQ_HINTS[@]}"; do
295
+ label="${entry%%|*}"
296
+ cmd="${entry#*|}"
297
+ printf ' %s%s%s\n' "$BOLD" "$label" "$RESET"
298
+ printf ' %s\n' "$cmd"
299
+ done
300
+ STEP=$((STEP + 1))
301
+ fi
302
+ if [ "${#COMPLETIONS_INSTALLED[@]}" -gt 0 ]; then
303
+ shells_csv="$(IFS=', '; printf '%s' "${COMPLETIONS_INSTALLED[*]}")"
304
+ printf ' %d. Activate tab completion (installed for: %s):\n' "$STEP" "$shells_csv"
305
+ printf ' exec $SHELL\n'
306
+ printf ' Verify by typing: celilo <TAB><TAB>\n'
307
+ if [ "${#COMPLETION_RC_SNIPPETS[@]}" -gt 0 ]; then
308
+ printf ' If completion does not work, append the matching rc snippet first:\n'
309
+ for entry in "${COMPLETION_RC_SNIPPETS[@]}"; do
310
+ shell_name="${entry%%|*}"
311
+ rest="${entry#*|}"
312
+ rc_line="${rest%%|*}"
313
+ rc_target="${rest##*|}"
314
+ printf ' %s%s%s — %s%s%s:\n' "$BOLD" "$shell_name" "$RESET" "$BOLD" "$rc_target" "$RESET"
315
+ printf '%s\n' "$rc_line" | sed 's/^/ /'
316
+ done
317
+ fi
318
+ STEP=$((STEP + 1))
319
+ fi
320
+ printf ' %d. Initialize the Celilo database and network config (interactive):\n' "$STEP"
321
+ printf ' celilo system init\n'
322
+ STEP=$((STEP + 1))
323
+ printf ' %d. (Optional) Install the event-bus daemon to stream deploy progress\n' "$STEP"
324
+ printf ' through a persistent system service:\n'
325
+ printf ' celilo events install-daemon\n'
326
+ STEP=$((STEP + 1))
327
+ printf ' %d. Add an infrastructure provider (Proxmox, Digital Ocean, ...):\n' "$STEP"
328
+ printf ' celilo service add\n'
329
+ STEP=$((STEP + 1))
330
+ printf ' %d. Full walkthrough: https://celilo.computer/docs\n' "$STEP"
@@ -0,0 +1,86 @@
1
+ <!DOCTYPE html><html lang="en" data-astro-cid-5hce7sga> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="description" content="Browse the official Celilo module registry."><title>Modules — Celilo</title><link rel="icon" type="image/svg+xml" href="/favicon.svg"><script>
2
+ // Resolve theme BEFORE first paint to avoid the flash of wrong
3
+ // theme. Order: explicit user choice (localStorage) → system
4
+ // preference (prefers-color-scheme) → fallback dark. The
5
+ // resolved value lands on <html data-theme="...">; CSS variables
6
+ // below pick the matching palette.
7
+ (function () {
8
+ try {
9
+ const stored = localStorage.getItem('celilo-theme');
10
+ const sys =
11
+ window.matchMedia &&
12
+ window.matchMedia('(prefers-color-scheme: light)').matches
13
+ ? 'light'
14
+ : 'dark';
15
+ const theme = stored === 'light' || stored === 'dark' ? stored : sys;
16
+ document.documentElement.setAttribute('data-theme', theme);
17
+ } catch (_e) {
18
+ document.documentElement.setAttribute('data-theme', 'dark');
19
+ }
20
+ })();
21
+ </script><link rel="stylesheet" href="/_astro/docs.DwsUauWZ.css">
22
+ <style>.page-header[data-astro-cid-ywq4tbty]{padding:1.5rem 0;border-bottom:1px solid var(--border);margin-bottom:2rem}.page-header[data-astro-cid-ywq4tbty] h1[data-astro-cid-ywq4tbty]{font-size:2.5rem;font-weight:800;letter-spacing:-.03em;margin-bottom:.75rem}.page-header[data-astro-cid-ywq4tbty] p[data-astro-cid-ywq4tbty]{color:var(--muted);font-size:1.05rem}.search-bar[data-astro-cid-ywq4tbty]{margin-bottom:2rem;display:flex;gap:.75rem;flex-wrap:wrap;align-items:center}.search-bar[data-astro-cid-ywq4tbty] input[data-astro-cid-ywq4tbty],.search-bar[data-astro-cid-ywq4tbty] select[data-astro-cid-ywq4tbty]{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:.625rem 1rem;color:var(--text);font-size:.9rem;font-family:inherit;outline:none;transition:border-color .15s}.search-bar[data-astro-cid-ywq4tbty] input[data-astro-cid-ywq4tbty]{flex:1;min-width:14rem;max-width:28rem}.search-bar[data-astro-cid-ywq4tbty] select[data-astro-cid-ywq4tbty]{padding-right:2rem;cursor:pointer}.search-bar[data-astro-cid-ywq4tbty] input[data-astro-cid-ywq4tbty]:focus,.search-bar[data-astro-cid-ywq4tbty] select[data-astro-cid-ywq4tbty]:focus{border-color:var(--accent)}.search-bar[data-astro-cid-ywq4tbty] input[data-astro-cid-ywq4tbty]::placeholder{color:var(--muted)}.search-bar[data-astro-cid-ywq4tbty] label[data-astro-cid-ywq4tbty]{font-size:.85rem;color:var(--muted)}#module-grid[data-astro-cid-ywq4tbty]{display:grid;grid-template-columns:repeat(auto-fill,minmax(22rem,1fr));gap:1rem}.module-card[data-astro-cid-ywq4tbty]{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:1.5rem;display:flex;flex-direction:column;gap:.75rem;transition:border-color .15s}.module-card[data-astro-cid-ywq4tbty]:hover{border-color:var(--accent)}.module-header[data-astro-cid-ywq4tbty]{display:flex;align-items:baseline;gap:.75rem}.module-name[data-astro-cid-ywq4tbty]{font-family:var(--font-mono);font-size:1rem;font-weight:600;color:var(--accent-light);text-decoration:none}.module-name[data-astro-cid-ywq4tbty]:hover{text-decoration:underline}.module-version[data-astro-cid-ywq4tbty]{font-family:var(--font-mono);font-size:.75rem;color:var(--muted);background:var(--bg);border:1px solid var(--border);padding:.1em .5em;border-radius:4px}.module-description[data-astro-cid-ywq4tbty]{color:var(--muted);font-size:.875rem;line-height:1.6;flex:1}.module-tags[data-astro-cid-ywq4tbty]{display:flex;flex-wrap:wrap;gap:.375rem}.tag[data-astro-cid-ywq4tbty]{font-size:.7rem;padding:.2em .6em;border-radius:100px;background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent);font-family:var(--font-mono)}.module-footer[data-astro-cid-ywq4tbty]{display:flex;justify-content:space-between;align-items:center;font-size:.8rem;color:var(--muted)}.download-count[data-astro-cid-ywq4tbty]{font-family:var(--font-mono);font-size:.7rem;color:var(--muted);display:inline-flex;align-items:center;gap:.3rem}.module-install[data-astro-cid-ywq4tbty]{font-family:var(--font-mono);font-size:.775rem;background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:.3rem .6rem;cursor:pointer;color:var(--green);display:flex;align-items:center;gap:.4rem}.module-install[data-astro-cid-ywq4tbty]:hover{border-color:var(--green)}#loading[data-astro-cid-ywq4tbty]{color:var(--muted);padding:3rem 0;text-align:center;font-size:.9rem}#error[data-astro-cid-ywq4tbty]{display:none;color:#dc2626;padding:1.5rem;background:color-mix(in srgb,#dc2626 8%,transparent);border:1px solid color-mix(in srgb,#dc2626 25%,transparent);border-radius:8px;font-size:.875rem}
23
+ </style></head> <body data-astro-cid-5hce7sga> <!--
24
+ Background pattern — vertical streams of falling water drawn in
25
+ the geometric vocabulary of a Greek meander. The <pattern>
26
+ tile (240×240) repeats seamlessly across the viewport. Strokes
27
+ use `currentColor` so the .bg-pattern CSS rule above can tint
28
+ them via `color: var(--bg-pattern-color)` without rebuilding
29
+ the SVG. To change the streams themselves, edit the four <path>
30
+ elements below.
31
+ --> <svg class="bg-pattern" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" data-astro-cid-5hce7sga> <defs data-astro-cid-5hce7sga> <pattern id="celilo-streams" width="240" height="240" patternUnits="userSpaceOnUse" data-astro-cid-5hce7sga> <!-- Streams flow downward — the patternTransform shifts the
32
+ pattern origin by one full tile (240px) over `dur`, then
33
+ loops seamlessly. Edit `dur` for speed; delete this
34
+ <animateTransform> to freeze the flow. --> <animateTransform attributeName="patternTransform" type="translate" from="0 0" to="0 240" dur="30s" repeatCount="indefinite" data-astro-cid-5hce7sga></animateTransform> <g fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" data-astro-cid-5hce7sga> <!-- Stroke color cycle — driven by SMIL inside the SVG
35
+ (not CSS `color`) because Chromium doesn't reliably
36
+ repaint strokes that resolve via currentColor when
37
+ the parent's CSS `color` is animated. Edit the
38
+ `values` list to pick your palette; semicolons
39
+ separate stops, last value should match first to
40
+ close the loop seamlessly. --> <animate attributeName="stroke" values="#818cf8;#f472b6;#4ade80;#22d3ee;#818cf8" dur="10s" repeatCount="indefinite" data-astro-cid-5hce7sga></animate> <!-- Stream A (x=40): big staircase cascade near top, single hook near bottom --> <path d="M 40,0 V 30 H 55 V 45 H 70 V 60 H 55 V 75 H 40 V 175 H 25 V 195 H 40 V 240" data-astro-cid-5hce7sga></path> <!-- Stream B (x=100): small step near top, three-tier cascade in middle --> <path d="M 100,0 V 60 H 85 V 80 H 100 V 130 H 115 V 145 H 130 V 160 H 115 V 175 H 100 V 240" data-astro-cid-5hce7sga></path> <!-- Stream C (x=160): three small hooks distributed top-to-bottom --> <path d="M 160,0 V 20 H 175 V 35 H 160 V 110 H 145 V 130 H 160 V 200 H 175 V 220 H 160 V 240" data-astro-cid-5hce7sga></path> <!-- Stream D (x=220): single deep cascade in the upper-middle --> <path d="M 220,0 V 85 H 205 V 100 H 190 V 115 H 220 V 240" data-astro-cid-5hce7sga></path> </g> </pattern> </defs> <rect width="100%" height="100%" fill="url(#celilo-streams)" data-astro-cid-5hce7sga></rect> </svg> <nav data-astro-cid-5hce7sga> <div class="nav-inner" data-astro-cid-5hce7sga> <a href="/" class="nav-logo" data-astro-cid-5hce7sga>celilo</a> <ul class="nav-links" data-astro-cid-5hce7sga> <li data-astro-cid-5hce7sga> <a href="/docs" data-astro-cid-5hce7sga> <svg viewBox="0 0 24 24" aria-hidden="true" data-astro-cid-5hce7sga> <path d="M4 5a2 2 0 0 1 2-2h11v18H6a2 2 0 0 1-2-2V5z" data-astro-cid-5hce7sga></path> <path d="M9 7h6M9 11h6M9 15h4" data-astro-cid-5hce7sga></path> </svg>
41
+ docs
42
+ </a> </li> <li data-astro-cid-5hce7sga> <a href="/modules" data-astro-cid-5hce7sga> <svg viewBox="0 0 24 24" aria-hidden="true" data-astro-cid-5hce7sga> <rect x="3" y="3" width="7" height="7" rx="1" data-astro-cid-5hce7sga></rect> <rect x="14" y="3" width="7" height="7" rx="1" data-astro-cid-5hce7sga></rect> <rect x="3" y="14" width="7" height="7" rx="1" data-astro-cid-5hce7sga></rect> <rect x="14" y="14" width="7" height="7" rx="1" data-astro-cid-5hce7sga></rect> </svg>
43
+ modules
44
+ </a> </li> <li data-astro-cid-5hce7sga> <a href="/vision" data-astro-cid-5hce7sga> <svg viewBox="0 0 24 24" aria-hidden="true" data-astro-cid-5hce7sga> <path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12z" data-astro-cid-5hce7sga></path> <circle cx="12" cy="12" r="3" data-astro-cid-5hce7sga></circle> </svg>
45
+ vision
46
+ </a> </li> <li data-astro-cid-5hce7sga> <a href="https://github.com/celilo/celilo" target="_blank" rel="noopener" data-astro-cid-5hce7sga> <svg viewBox="0 0 24 24" aria-hidden="true" data-astro-cid-5hce7sga> <path d="M9 19c-4.3 1.4-4.3-2.5-6-3m12 5v-3.5c0-1 .1-1.4-.5-2 2.8-.3 5.5-1.4 5.5-6a4.6 4.6 0 0 0-1.3-3.2 4.2 4.2 0 0 0-.1-3.2s-1.1-.3-3.5 1.3a12.3 12.3 0 0 0-6.2 0C6.5 2.8 5.4 3.1 5.4 3.1a4.2 4.2 0 0 0-.1 3.2A4.6 4.6 0 0 0 4 9.5c0 4.6 2.7 5.7 5.5 6-.6.6-.6 1.2-.5 2V21" data-astro-cid-5hce7sga></path> </svg>
47
+ github
48
+ </a> </li> <li data-astro-cid-5hce7sga> <button type="button" class="theme-toggle" id="theme-toggle" aria-label="Toggle light/dark theme" title="Toggle theme" data-astro-cid-5hce7sga> <svg class="icon-sun" viewBox="0 0 24 24" aria-hidden="true" data-astro-cid-5hce7sga> <circle cx="12" cy="12" r="4" data-astro-cid-5hce7sga></circle> <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41" data-astro-cid-5hce7sga></path> </svg> <svg class="icon-moon" viewBox="0 0 24 24" aria-hidden="true" data-astro-cid-5hce7sga> <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" data-astro-cid-5hce7sga></path> </svg> </button> </li> </ul> </div> </nav> <script>
49
+ // Click handler for the toggle. Reads the current data-theme,
50
+ // flips it, persists to localStorage, applies to <html>. The
51
+ // resolution script in <head> already set the initial value
52
+ // (system-aware) before paint, so this only runs on user
53
+ // interaction.
54
+ (function () {
55
+ const btn = document.getElementById('theme-toggle');
56
+ if (!btn) return;
57
+ btn.addEventListener('click', function () {
58
+ const current = document.documentElement.getAttribute('data-theme');
59
+ const next = current === 'light' ? 'dark' : 'light';
60
+ document.documentElement.setAttribute('data-theme', next);
61
+ try {
62
+ localStorage.setItem('celilo-theme', next);
63
+ } catch (_e) {
64
+ // localStorage unavailable (private mode) — toggle still
65
+ // works for the session, just doesn't persist.
66
+ }
67
+ });
68
+ })();
69
+ </script> <main data-astro-cid-5hce7sga> <div class="page-header" data-astro-cid-ywq4tbty> <h1 data-astro-cid-ywq4tbty>Module registry</h1> <p data-astro-cid-ywq4tbty>Official modules for Celilo. Install any module with <code data-astro-cid-ywq4tbty>celilo module import &lt;name&gt;</code>.</p> </div> <div class="search-bar" data-astro-cid-ywq4tbty> <input id="search" type="search" placeholder="Search modules..." autocomplete="off" data-astro-cid-ywq4tbty> <label for="sort" data-astro-cid-ywq4tbty>Sort:</label> <select id="sort" data-astro-cid-ywq4tbty> <option value="name" data-astro-cid-ywq4tbty>Alphabetical</option> <option value="downloads" data-astro-cid-ywq4tbty>Most downloaded</option> </select> </div> <div id="error" data-astro-cid-ywq4tbty></div> <div id="loading" data-astro-cid-ywq4tbty>Loading modules...</div> <div id="module-grid" style="display:none" data-astro-cid-ywq4tbty></div> <script type="module">const d=document.getElementById("module-grid"),c=document.getElementById("loading"),r=document.getElementById("error"),a=document.getElementById("search"),m=document.getElementById("sort");async function l(t=""){try{const e=new URLSearchParams({per_page:"50"});t&&e.set("q",t),m.value==="downloads"&&e.set("sort","downloads");const o=await fetch(`/registry/api/v1/modules?${e}`);if(!o.ok)throw new Error(`Registry returned ${o.status}`);const n=await o.json();p(n.modules)}catch(e){r.textContent=`Could not reach registry: ${e instanceof Error?e.message:String(e)}`,r.style.display="block",c.style.display="none"}}function p(t){c.style.display="none",d.style.display="grid",d.innerHTML=t.length===0?'<p style="color:var(--muted);grid-column:1/-1">No modules found.</p>':t.map(e=>g(e)).join("")}function s(t){const e=document.createElement("div");return e.textContent=t,e.innerHTML}function g(t){const e=`celilo module import ${t.name}`,o=`/modules/view?name=${encodeURIComponent(t.name)}`,n=t.total_downloads,u=n===1?"1 download":`${n.toLocaleString()} downloads`;return`
70
+ <div class="module-card">
71
+ <div class="module-header">
72
+ <a href="${o}" class="module-name">${s(t.name)}</a>
73
+ <span class="module-version">${s(t.max_version)}</span>
74
+ </div>
75
+ <p class="module-description">${s(t.description||"No description.")}</p>
76
+ <div class="module-footer">
77
+ <button class="module-install" onclick="navigator.clipboard.writeText('${e}')" title="Copy install command">
78
+ 📋 ${e}
79
+ </button>
80
+ <span class="download-count" title="Total downloads">⬇ ${u}</span>
81
+ </div>
82
+ <div style="text-align:right;margin-top:-0.25rem;">
83
+ <a href="${o}" style="color:var(--muted);font-size:0.75rem;">details →</a>
84
+ </div>
85
+ </div>
86
+ `}let i;a.addEventListener("input",()=>{clearTimeout(i),i=setTimeout(()=>l(a.value.trim()),300)});m.addEventListener("change",()=>{l(a.value.trim())});l();</script> </main> <footer data-astro-cid-5hce7sga> <p data-astro-cid-5hce7sga>Celilo — home lab orchestration. Built with care.</p> </footer> </body></html>
@@ -0,0 +1,69 @@
1
+ <!DOCTYPE html><html lang="en" data-astro-cid-5hce7sga> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="description" content="Per-module detail in the Celilo registry."><title>Module — Celilo</title><link rel="icon" type="image/svg+xml" href="/favicon.svg"><script>
2
+ // Resolve theme BEFORE first paint to avoid the flash of wrong
3
+ // theme. Order: explicit user choice (localStorage) → system
4
+ // preference (prefers-color-scheme) → fallback dark. The
5
+ // resolved value lands on <html data-theme="...">; CSS variables
6
+ // below pick the matching palette.
7
+ (function () {
8
+ try {
9
+ const stored = localStorage.getItem('celilo-theme');
10
+ const sys =
11
+ window.matchMedia &&
12
+ window.matchMedia('(prefers-color-scheme: light)').matches
13
+ ? 'light'
14
+ : 'dark';
15
+ const theme = stored === 'light' || stored === 'dark' ? stored : sys;
16
+ document.documentElement.setAttribute('data-theme', theme);
17
+ } catch (_e) {
18
+ document.documentElement.setAttribute('data-theme', 'dark');
19
+ }
20
+ })();
21
+ </script><link rel="stylesheet" href="/_astro/docs.DwsUauWZ.css">
22
+ <style>.page-header[data-astro-cid-6o2hlio5]{padding:1.5rem 0;border-bottom:1px solid var(--border);margin-bottom:2rem}.page-header[data-astro-cid-6o2hlio5] .breadcrumb[data-astro-cid-6o2hlio5]{font-size:.85rem;color:var(--muted);margin-bottom:.5rem}.page-header[data-astro-cid-6o2hlio5] .breadcrumb[data-astro-cid-6o2hlio5] a[data-astro-cid-6o2hlio5]{color:var(--muted)}.page-header[data-astro-cid-6o2hlio5] .breadcrumb[data-astro-cid-6o2hlio5] a[data-astro-cid-6o2hlio5]:hover{color:var(--text)}.page-header[data-astro-cid-6o2hlio5] h1[data-astro-cid-6o2hlio5]{font-family:var(--font-mono);font-size:2rem;font-weight:700;letter-spacing:-.02em;color:var(--accent-light);margin-bottom:.5rem}.page-header[data-astro-cid-6o2hlio5] .summary[data-astro-cid-6o2hlio5]{color:var(--muted);font-size:1.05rem}.stats[data-astro-cid-6o2hlio5]{display:flex;gap:1.5rem;margin-top:.75rem;font-size:.85rem;color:var(--muted)}.stats[data-astro-cid-6o2hlio5] span[data-astro-cid-6o2hlio5]{display:inline-flex;align-items:center;gap:.4rem}.stats[data-astro-cid-6o2hlio5] strong[data-astro-cid-6o2hlio5]{color:var(--text);font-family:var(--font-mono);font-weight:600}.install-block[data-astro-cid-6o2hlio5]{margin:1.5rem 0 2rem}.install-label[data-astro-cid-6o2hlio5]{font-size:.85rem;color:var(--muted);margin-bottom:.5rem}.install-pre[data-astro-cid-6o2hlio5]{font-family:var(--font-mono);background:var(--surface);border:1px solid var(--border);border-radius:6px;padding:.75rem 1rem;display:flex;justify-content:space-between;align-items:center;gap:1rem;font-size:.9rem;color:var(--green);overflow-x:auto}.install-pre[data-astro-cid-6o2hlio5] button[data-astro-cid-6o2hlio5]{flex-shrink:0;background:transparent;border:1px solid var(--border);border-radius:4px;color:var(--muted);cursor:pointer;font-family:inherit;font-size:.75rem;padding:.25rem .6rem}.install-pre[data-astro-cid-6o2hlio5] button[data-astro-cid-6o2hlio5]:hover{color:var(--text);border-color:var(--text)}h2[data-astro-cid-6o2hlio5]{font-size:1.15rem;font-weight:600;margin-bottom:.75rem;color:var(--text)}.versions[data-astro-cid-6o2hlio5]{list-style:none;border:1px solid var(--border);border-radius:8px;background:var(--surface);overflow:hidden}.versions[data-astro-cid-6o2hlio5] li[data-astro-cid-6o2hlio5]{display:flex;justify-content:space-between;align-items:center;padding:.5rem 1rem;border-bottom:1px solid var(--border);font-size:.85rem}.versions[data-astro-cid-6o2hlio5] li[data-astro-cid-6o2hlio5]:last-child{border-bottom:none}.version-num[data-astro-cid-6o2hlio5]{font-family:var(--font-mono);color:var(--accent-light)}.version-num[data-astro-cid-6o2hlio5].latest:after{content:"latest";font-family:var(--font-sans);font-size:.65rem;color:var(--green);background:color-mix(in srgb,var(--green) 12%,transparent);border:1px solid color-mix(in srgb,var(--green) 30%,transparent);padding:.1em .5em;border-radius:100px;margin-left:.6rem;font-weight:500}.version-num[data-astro-cid-6o2hlio5].yanked{color:var(--muted);text-decoration:line-through}.yanked-tag[data-astro-cid-6o2hlio5]{font-size:.7rem;color:#dc2626;font-family:var(--font-sans)}#loading[data-astro-cid-6o2hlio5]{color:var(--muted);padding:3rem 0;text-align:center;font-size:.9rem}#error[data-astro-cid-6o2hlio5]{display:none;color:#dc2626;padding:1.5rem;background:color-mix(in srgb,#dc2626 8%,transparent);border:1px solid color-mix(in srgb,#dc2626 25%,transparent);border-radius:8px;font-size:.875rem}#content[data-astro-cid-6o2hlio5]{display:none}
23
+ </style></head> <body data-astro-cid-5hce7sga> <!--
24
+ Background pattern — vertical streams of falling water drawn in
25
+ the geometric vocabulary of a Greek meander. The <pattern>
26
+ tile (240×240) repeats seamlessly across the viewport. Strokes
27
+ use `currentColor` so the .bg-pattern CSS rule above can tint
28
+ them via `color: var(--bg-pattern-color)` without rebuilding
29
+ the SVG. To change the streams themselves, edit the four <path>
30
+ elements below.
31
+ --> <svg class="bg-pattern" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" data-astro-cid-5hce7sga> <defs data-astro-cid-5hce7sga> <pattern id="celilo-streams" width="240" height="240" patternUnits="userSpaceOnUse" data-astro-cid-5hce7sga> <!-- Streams flow downward — the patternTransform shifts the
32
+ pattern origin by one full tile (240px) over `dur`, then
33
+ loops seamlessly. Edit `dur` for speed; delete this
34
+ <animateTransform> to freeze the flow. --> <animateTransform attributeName="patternTransform" type="translate" from="0 0" to="0 240" dur="30s" repeatCount="indefinite" data-astro-cid-5hce7sga></animateTransform> <g fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" data-astro-cid-5hce7sga> <!-- Stroke color cycle — driven by SMIL inside the SVG
35
+ (not CSS `color`) because Chromium doesn't reliably
36
+ repaint strokes that resolve via currentColor when
37
+ the parent's CSS `color` is animated. Edit the
38
+ `values` list to pick your palette; semicolons
39
+ separate stops, last value should match first to
40
+ close the loop seamlessly. --> <animate attributeName="stroke" values="#818cf8;#f472b6;#4ade80;#22d3ee;#818cf8" dur="10s" repeatCount="indefinite" data-astro-cid-5hce7sga></animate> <!-- Stream A (x=40): big staircase cascade near top, single hook near bottom --> <path d="M 40,0 V 30 H 55 V 45 H 70 V 60 H 55 V 75 H 40 V 175 H 25 V 195 H 40 V 240" data-astro-cid-5hce7sga></path> <!-- Stream B (x=100): small step near top, three-tier cascade in middle --> <path d="M 100,0 V 60 H 85 V 80 H 100 V 130 H 115 V 145 H 130 V 160 H 115 V 175 H 100 V 240" data-astro-cid-5hce7sga></path> <!-- Stream C (x=160): three small hooks distributed top-to-bottom --> <path d="M 160,0 V 20 H 175 V 35 H 160 V 110 H 145 V 130 H 160 V 200 H 175 V 220 H 160 V 240" data-astro-cid-5hce7sga></path> <!-- Stream D (x=220): single deep cascade in the upper-middle --> <path d="M 220,0 V 85 H 205 V 100 H 190 V 115 H 220 V 240" data-astro-cid-5hce7sga></path> </g> </pattern> </defs> <rect width="100%" height="100%" fill="url(#celilo-streams)" data-astro-cid-5hce7sga></rect> </svg> <nav data-astro-cid-5hce7sga> <div class="nav-inner" data-astro-cid-5hce7sga> <a href="/" class="nav-logo" data-astro-cid-5hce7sga>celilo</a> <ul class="nav-links" data-astro-cid-5hce7sga> <li data-astro-cid-5hce7sga> <a href="/docs" data-astro-cid-5hce7sga> <svg viewBox="0 0 24 24" aria-hidden="true" data-astro-cid-5hce7sga> <path d="M4 5a2 2 0 0 1 2-2h11v18H6a2 2 0 0 1-2-2V5z" data-astro-cid-5hce7sga></path> <path d="M9 7h6M9 11h6M9 15h4" data-astro-cid-5hce7sga></path> </svg>
41
+ docs
42
+ </a> </li> <li data-astro-cid-5hce7sga> <a href="/modules" data-astro-cid-5hce7sga> <svg viewBox="0 0 24 24" aria-hidden="true" data-astro-cid-5hce7sga> <rect x="3" y="3" width="7" height="7" rx="1" data-astro-cid-5hce7sga></rect> <rect x="14" y="3" width="7" height="7" rx="1" data-astro-cid-5hce7sga></rect> <rect x="3" y="14" width="7" height="7" rx="1" data-astro-cid-5hce7sga></rect> <rect x="14" y="14" width="7" height="7" rx="1" data-astro-cid-5hce7sga></rect> </svg>
43
+ modules
44
+ </a> </li> <li data-astro-cid-5hce7sga> <a href="/vision" data-astro-cid-5hce7sga> <svg viewBox="0 0 24 24" aria-hidden="true" data-astro-cid-5hce7sga> <path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12z" data-astro-cid-5hce7sga></path> <circle cx="12" cy="12" r="3" data-astro-cid-5hce7sga></circle> </svg>
45
+ vision
46
+ </a> </li> <li data-astro-cid-5hce7sga> <a href="https://github.com/celilo/celilo" target="_blank" rel="noopener" data-astro-cid-5hce7sga> <svg viewBox="0 0 24 24" aria-hidden="true" data-astro-cid-5hce7sga> <path d="M9 19c-4.3 1.4-4.3-2.5-6-3m12 5v-3.5c0-1 .1-1.4-.5-2 2.8-.3 5.5-1.4 5.5-6a4.6 4.6 0 0 0-1.3-3.2 4.2 4.2 0 0 0-.1-3.2s-1.1-.3-3.5 1.3a12.3 12.3 0 0 0-6.2 0C6.5 2.8 5.4 3.1 5.4 3.1a4.2 4.2 0 0 0-.1 3.2A4.6 4.6 0 0 0 4 9.5c0 4.6 2.7 5.7 5.5 6-.6.6-.6 1.2-.5 2V21" data-astro-cid-5hce7sga></path> </svg>
47
+ github
48
+ </a> </li> <li data-astro-cid-5hce7sga> <button type="button" class="theme-toggle" id="theme-toggle" aria-label="Toggle light/dark theme" title="Toggle theme" data-astro-cid-5hce7sga> <svg class="icon-sun" viewBox="0 0 24 24" aria-hidden="true" data-astro-cid-5hce7sga> <circle cx="12" cy="12" r="4" data-astro-cid-5hce7sga></circle> <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41" data-astro-cid-5hce7sga></path> </svg> <svg class="icon-moon" viewBox="0 0 24 24" aria-hidden="true" data-astro-cid-5hce7sga> <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" data-astro-cid-5hce7sga></path> </svg> </button> </li> </ul> </div> </nav> <script>
49
+ // Click handler for the toggle. Reads the current data-theme,
50
+ // flips it, persists to localStorage, applies to <html>. The
51
+ // resolution script in <head> already set the initial value
52
+ // (system-aware) before paint, so this only runs on user
53
+ // interaction.
54
+ (function () {
55
+ const btn = document.getElementById('theme-toggle');
56
+ if (!btn) return;
57
+ btn.addEventListener('click', function () {
58
+ const current = document.documentElement.getAttribute('data-theme');
59
+ const next = current === 'light' ? 'dark' : 'light';
60
+ document.documentElement.setAttribute('data-theme', next);
61
+ try {
62
+ localStorage.setItem('celilo-theme', next);
63
+ } catch (_e) {
64
+ // localStorage unavailable (private mode) — toggle still
65
+ // works for the session, just doesn't persist.
66
+ }
67
+ });
68
+ })();
69
+ </script> <main data-astro-cid-5hce7sga> <div id="error" data-astro-cid-6o2hlio5></div> <div id="loading" data-astro-cid-6o2hlio5>Loading module...</div> <div id="content" data-astro-cid-6o2hlio5> <div class="page-header" data-astro-cid-6o2hlio5> <div class="breadcrumb" data-astro-cid-6o2hlio5><a href="/modules" data-astro-cid-6o2hlio5>← all modules</a></div> <h1 id="m-name" data-astro-cid-6o2hlio5></h1> <p class="summary" id="m-description" data-astro-cid-6o2hlio5></p> <div class="stats" data-astro-cid-6o2hlio5> <span data-astro-cid-6o2hlio5>⬇ <strong id="m-downloads" data-astro-cid-6o2hlio5></strong> downloads</span> <span data-astro-cid-6o2hlio5>📦 <strong id="m-version-count" data-astro-cid-6o2hlio5></strong> versions</span> </div> </div> <div class="install-block" data-astro-cid-6o2hlio5> <div class="install-label" data-astro-cid-6o2hlio5>Install</div> <div class="install-pre" data-astro-cid-6o2hlio5> <code id="m-install" data-astro-cid-6o2hlio5></code> <button id="m-copy" type="button" data-astro-cid-6o2hlio5>Copy</button> </div> </div> <h2 data-astro-cid-6o2hlio5>Versions</h2> <ul class="versions" id="m-versions" data-astro-cid-6o2hlio5></ul> </div> <script type="module">const r=document.getElementById("loading"),d=document.getElementById("error"),y=document.getElementById("content");function s(e){d.textContent=e,d.style.display="block",r.style.display="none"}function g(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML}async function p(){const e=new URLSearchParams(window.location.search).get("name");if(!e){s("No module name in URL. Try /modules/view?name=<module>.");return}try{const t=await fetch(`/registry/api/v1/modules/${encodeURIComponent(e)}`);if(t.status===404){s(`Module "${e}" not found in the registry.`);return}if(!t.ok){s(`Registry returned ${t.status}.`);return}const o=await t.json();E(o)}catch(t){s(`Could not reach registry: ${t instanceof Error?t.message:String(t)}`)}}function E(e){document.title=`${e.name} — Celilo`,document.getElementById("m-name").textContent=e.name,document.getElementById("m-description").textContent=e.description||"No description.",document.getElementById("m-downloads").textContent=(e.total_downloads??0).toLocaleString(),document.getElementById("m-version-count").textContent=e.versions.length.toString();const t=`celilo module import ${e.name}`;document.getElementById("m-install").textContent=t;const o=document.getElementById("m-copy");o.addEventListener("click",()=>{navigator.clipboard.writeText(t),o.textContent="Copied!",setTimeout(()=>{o.textContent="Copy"},1200)});const i=[...e.versions].reverse(),c=i.find(n=>!n.yanked),l=document.getElementById("m-versions");l.innerHTML=i.map(n=>{const m=n===c,a=["version-num"];m&&a.push("latest"),n.yanked&&a.push("yanked");const u=n.yanked?'<span class="yanked-tag">yanked</span>':"<span></span>";return`<li><span class="${a.join(" ")}">${g(n.num)}</span>${u}</li>`}).join(""),r.style.display="none",y.style.display="block"}p();</script> </main> <footer data-astro-cid-5hce7sga> <p data-astro-cid-5hce7sga>Celilo — home lab orchestration. Built with care.</p> </footer> </body></html>