docs: deployment scaffolding + logos + live cluster examples

Three additions to the docs site, all atomic to docs/:

1. Deployment configs (Dockerfile + Caddyfile + docker-compose.yml +
   .env.example + Makefile) mirroring bingham/cucx's pattern. The
   compose service uses caddy-docker-proxy labels with the operator's
   .mcp.l.supported.systems wildcard DNS pattern; suggested subdomain
   is mcaxl-docs.mcp.l.supported.systems.

2. Logo + favicon (forest-green palette matching the existing custom.css
   accent). Wordmark uses ui-monospace with currentColor so Starlight
   inverts on light/dark; icon-mark is a terminal chevron + three
   diminishing query-row lines (audit-by-query motif).

3. Live cluster examples in reference/tools.md for axl_version,
   axl_list_tables (route% pattern), and axl_describe_table
   (routepartition). Outputs sanitized per python.md PII rules
   (15.0.1.12900(234) → 15.0(1); cluster-fingerprinting build string
   removed).

Build clean: 17 pages built, pagefind search index across all,
favicon resolves to /favicon.svg, logo fingerprinted into _astro/.

Not yet deployed — operator wires docker compose up when ready.
This commit is contained in:
Ryan Malloy 2026-04-29 04:19:20 -06:00
parent f060170e90
commit 314a80d6de
10 changed files with 321 additions and 4 deletions

10
docs/.dockerignore Normal file
View File

@ -0,0 +1,10 @@
node_modules
dist
.astro
.git
.gitignore
.env
.env.*
!.env.example
*.log
.DS_Store

18
docs/.env.example Normal file
View File

@ -0,0 +1,18 @@
# mcaxl docs — local environment.
#
# Copy to .env and adjust. `.env` is gitignored.
# Distinguishes this compose project from others on the host.
COMPOSE_PROJECT=mcaxl-docs
# Public hostnames (resolved via the wildcard DNS for *.mcp.l.supported.systems
# that already points at the host's edge caddy-docker-proxy).
# DOMAIN → prod static build
# DEV_DOMAIN → dev server, hot-reload
DOMAIN=mcaxl-docs.mcp.l.supported.systems
DEV_DOMAIN=dev-mcaxl-docs.mcp.l.supported.systems
# Mode switch — drives which compose profile is active.
# dev → astro dev server + HMR (hot reload) at DEV_DOMAIN
# prod → static build served by in-container Caddy at DOMAIN
COMPOSE_PROFILES=dev

31
docs/Caddyfile Normal file
View File

@ -0,0 +1,31 @@
# Internal Caddy used by the prod container to serve the static site.
# caddy-docker-proxy terminates TLS at the edge, forwards to :80 here.
:80 {
root * /srv
file_server
encode gzip zstd
# Long-cache everything Astro puts under /_astro/ (hashed filenames)
@immutable path /_astro/*
header @immutable Cache-Control "public, max-age=31536000, immutable"
# Short-cache HTML so content updates appear quickly after redeploy.
@html path_regexp htmlrx \.(html)?$
header @html Cache-Control "public, max-age=60, must-revalidate"
# Basic security headers
header {
X-Content-Type-Options nosniff
Referrer-Policy strict-origin-when-cross-origin
X-Frame-Options SAMEORIGIN
-Server
}
# Render 404 for missing pages
handle_errors {
@404 expression {http.error.status_code} == 404
rewrite @404 /404.html
file_server
}
}

62
docs/Dockerfile Normal file
View File

@ -0,0 +1,62 @@
# mcaxl docs — multi-stage build.
#
# Stages:
# deps — install node_modules once, cached by package.json
# dev — runs `astro dev` with the source bind-mounted (compose `dev` profile)
# build — produces the static site in /app/dist
# prod — serves /app/dist with Caddy (static file server, no Node runtime)
# ---------- Stage: deps --------------------------------------------------
FROM mirror.gcr.io/library/node:22-alpine AS deps
ENV ASTRO_TELEMETRY_DISABLED=1 \
NODE_ENV=development
WORKDIR /app
COPY package.json package-lock.json* ./
RUN --mount=type=cache,target=/root/.npm \
npm install --no-audit --no-fund
# ---------- Stage: dev ---------------------------------------------------
FROM mirror.gcr.io/library/node:22-alpine AS dev
ENV ASTRO_TELEMETRY_DISABLED=1 \
NODE_ENV=development \
HOST=0.0.0.0
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
EXPOSE 4321
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
# ---------- Stage: build -------------------------------------------------
FROM mirror.gcr.io/library/node:22-alpine AS build
ENV ASTRO_TELEMETRY_DISABLED=1 \
NODE_ENV=production
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# DOMAIN must be set at build time so Astro bakes the canonical site URL
# (sitemaps, og: tags, etc.) into the static output.
ARG DOMAIN
ENV DOMAIN=${DOMAIN}
RUN npm run build
# ---------- Stage: prod --------------------------------------------------
# Static site served by Caddy. No Node in the final image.
FROM mirror.gcr.io/library/caddy:2-alpine AS prod
COPY --from=build /app/dist /srv
COPY Caddyfile /etc/caddy/Caddyfile
EXPOSE 80
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]

57
docs/Makefile Normal file
View File

@ -0,0 +1,57 @@
# mcaxl docs — compose wrapper.
#
# Mode (dev/prod) is driven by COMPOSE_PROFILES in .env. Flip it there.
# `docker compose` honors COMPOSE_PROJECT and COMPOSE_PROFILES automatically.
COMPOSE ?= docker compose
.PHONY: help up down restart logs shell ps build clean prod dev net pull-prod pull-dev
help: ## Show this help
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " \033[36m%-10s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
up: ## Start the active profile (set COMPOSE_PROFILES in .env)
$(COMPOSE) up -d --build
dev: ## Start dev explicitly (hot reload at https://$${DEV_DOMAIN})
COMPOSE_PROFILES=dev $(COMPOSE) up -d --build
prod: ## Start prod explicitly (static site at https://$${DOMAIN})
COMPOSE_PROFILES=prod $(COMPOSE) up -d --build
down: ## Stop and remove all containers for this project
COMPOSE_PROFILES=dev $(COMPOSE) down
COMPOSE_PROFILES=prod $(COMPOSE) down
restart: ## Restart the active profile
$(COMPOSE) restart
logs: ## Tail logs for active services
$(COMPOSE) logs -f --tail=200
shell: ## Shell into the dev container
$(COMPOSE) exec docs-dev sh
ps: ## Show running containers for this project
$(COMPOSE) ps
build: ## Build images for both profiles
COMPOSE_PROFILES=dev,prod $(COMPOSE) build
clean: ## Remove containers, images, and volumes for this project
COMPOSE_PROFILES=dev,prod $(COMPOSE) down --rmi local -v
net: ## Create the external `caddy` network (one-time, host-wide)
docker network inspect caddy >/dev/null 2>&1 || docker network create caddy
pull-prod: ## Deploy: git pull, rebuild, and restart the prod container
git pull --ff-only
COMPOSE_PROFILES=prod $(COMPOSE) up -d --build docs-prod
@echo
@COMPOSE_PROFILES=prod $(COMPOSE) ps docs-prod
pull-dev: ## Deploy: git pull, rebuild, and restart the dev container
git pull --ff-only
COMPOSE_PROFILES=dev $(COMPOSE) up -d --build docs-dev
@echo
@COMPOSE_PROFILES=dev $(COMPOSE) ps docs-dev

View File

@ -52,6 +52,7 @@ export default defineConfig({
src: './src/assets/logo.svg', src: './src/assets/logo.svg',
replacesTitle: false, replacesTitle: false,
}, },
favicon: '/favicon.svg',
customCss: ['./src/styles/custom.css'], customCss: ['./src/styles/custom.css'],
// Per-page action buttons: "Copy Markdown" + "View in Markdown" // Per-page action buttons: "Copy Markdown" + "View in Markdown"

71
docs/docker-compose.yml Normal file
View File

@ -0,0 +1,71 @@
# mcaxl docs — dev/prod via compose profiles.
#
# Mode is driven by COMPOSE_PROFILES in .env:
# COMPOSE_PROFILES=dev → hot-reload astro dev server at https://${DEV_DOMAIN}
# COMPOSE_PROFILES=prod → static site via in-container Caddy at https://${DOMAIN}
#
# Both profiles sit behind the host's external caddy-docker-proxy on the
# `caddy` network. mcaxl is a public OSS project; no basic auth.
#
# Prerequisite: `docker network create caddy` (one-time, shared with the
# edge caddy-docker-proxy instance).
services:
docs-dev:
profiles: ["dev"]
build:
context: .
dockerfile: Dockerfile
target: dev
image: ${COMPOSE_PROJECT}-dev
container_name: ${COMPOSE_PROJECT}-dev
restart: unless-stopped
environment:
ASTRO_TELEMETRY_DISABLED: "1"
NODE_ENV: development
DOMAIN: ${DEV_DOMAIN} # Vite HMR binds wss://${DEV_DOMAIN}:443
volumes:
- .:/app
- /app/node_modules
- /app/.astro
networks:
- caddy
labels:
caddy: ${DEV_DOMAIN}
caddy.reverse_proxy: "{{upstreams 4321}}"
# ── HMR-through-Caddy streaming tweaks ──
# Vite HMR doesn't send app-level pings; Caddy closes idle WebSockets
# after ~10-15s without these. Refs in ~/.claude/CLAUDE.md.
caddy.reverse_proxy.flush_interval: "-1"
caddy.reverse_proxy.transport: "http"
caddy.reverse_proxy.transport.read_timeout: "0"
caddy.reverse_proxy.transport.write_timeout: "0"
caddy.reverse_proxy.transport.keepalive: "5m"
caddy.reverse_proxy.transport.keepalive_idle_conns: "10"
caddy.reverse_proxy.stream_timeout: "24h"
caddy.reverse_proxy.stream_close_delay: "5s"
caddy.encode: "gzip zstd"
docs-prod:
profiles: ["prod"]
build:
context: .
dockerfile: Dockerfile
target: prod
args:
DOMAIN: ${DOMAIN}
image: ${COMPOSE_PROJECT}-prod
container_name: ${COMPOSE_PROJECT}-prod
restart: unless-stopped
networks:
- caddy
labels:
caddy: ${DOMAIN}
caddy.reverse_proxy: "{{upstreams 80}}"
caddy.encode: "gzip zstd"
networks:
caddy:
external: true

8
docs/public/favicon.svg Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="mcaxl">
<rect x="1" y="1" width="30" height="30" rx="6" fill="#103e34"/>
<path d="M7 11 L12 16 L7 21" fill="none" stroke="#4f9a78" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="15" y1="13" x2="26" y2="13" stroke="#a8cdb8" stroke-width="2" stroke-linecap="round"/>
<line x1="15" y1="17" x2="22" y2="17" stroke="#a8cdb8" stroke-width="2" stroke-linecap="round" opacity="0.75"/>
<line x1="15" y1="21" x2="24" y2="21" stroke="#a8cdb8" stroke-width="2" stroke-linecap="round" opacity="0.55"/>
</svg>

After

Width:  |  Height:  |  Size: 659 B

View File

@ -1,6 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" aria-hidden="true"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 260 44" role="img" aria-label="mcaxl">
<rect x="2" y="2" width="36" height="36" rx="8" fill="#1f6f5c"/> <!-- Icon mark: chevron prompt + stacked query rows (audit-by-query motif) -->
<path d="M10 12 L20 28 L30 12" stroke="#f3efe6" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none"/> <g transform="translate(4 6)">
<circle cx="20" cy="28" r="2.4" fill="#f3efe6"/> <rect x="0" y="0" width="32" height="32" rx="6" fill="none" stroke="#2f7d5e" stroke-width="2"/>
<!-- Terminal chevron -->
<path d="M6 10 L11 16 L6 22" fill="none" stroke="#4f9a78" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
<!-- Query rows (database lines) -->
<line x1="14" y1="13" x2="26" y2="13" stroke="#a8cdb8" stroke-width="2" stroke-linecap="round"/>
<line x1="14" y1="17" x2="22" y2="17" stroke="#a8cdb8" stroke-width="2" stroke-linecap="round" opacity="0.75"/>
<line x1="14" y1="21" x2="24" y2="21" stroke="#a8cdb8" stroke-width="2" stroke-linecap="round" opacity="0.55"/>
</g>
<!-- Wordmark -->
<text x="46" y="29" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="20" font-weight="600" fill="currentColor" letter-spacing="0.5">mcaxl</text>
<text x="118" y="29" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13" font-weight="400" fill="currentColor" opacity="0.55" letter-spacing="0.3">/ docs</text>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 372 B

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -26,6 +26,16 @@ change between cluster upgrades.
**Returns:** `{ version: str, _cache: "hit" | "miss" }` **Returns:** `{ version: str, _cache: "hit" | "miss" }`
**Live example** (against CUCM 15):
```json
{
"componentVersion": {
"version": "15.0(1)"
}
}
```
### `axl_sql` ### `axl_sql`
Execute a `SELECT` (or `WITH` CTE) against the CUCM Informix data Execute a `SELECT` (or `WITH` CTE) against the CUCM Informix data
@ -51,6 +61,24 @@ List Informix tables in the CUCM database.
|---|---|---| |---|---|---|
| `pattern` | `str \| None` | Optional LIKE pattern (`%` wildcards). e.g. `"route%"` finds `routelist`, `routegroup`, `routepartition`, `routefilter`. | | `pattern` | `str \| None` | Optional LIKE pattern (`%` wildcards). e.g. `"route%"` finds `routelist`, `routegroup`, `routepartition`, `routefilter`. |
**Live example** (`pattern="route%"` against CUCM 15):
```json
{
"table_count": 7,
"tables": [
"routefilter",
"routefiltercosroutingmap",
"routefiltermember",
"routegroup",
"routegroupdevicemap",
"routelist",
"routepartition"
],
"pattern": "route%"
}
```
### `axl_describe_table` ### `axl_describe_table`
Describe an Informix table's columns: name, type, length, nullability. Describe an Informix table's columns: name, type, length, nullability.
@ -63,6 +91,27 @@ table.
|---|---|---| |---|---|---|
| `table_name` | `str` | Exact Informix table name. | | `table_name` | `str` | Exact Informix table name. |
**Live example** (`table_name="routepartition"` against CUCM 15):
```json
{
"table": "routepartition",
"column_count": 11,
"columns": [
{ "name": "pkid", "type": "CHAR", "length": 36, "not_null": true },
{ "name": "name", "type": "VARCHAR", "length": 50, "not_null": true },
{ "name": "description", "type": "VARCHAR", "length": 200, "not_null": true },
{ "name": "fktimeschedule", "type": "CHAR", "length": 36, "not_null": false },
{ "name": "tktimezone", "type": "INTEGER", "length": 4, "not_null": true },
{ "name": "useoriginatingdevicetimezone", "type": "LVARCHAR", "length": 1, "not_null": true },
{ "name": "tkpartitionusage", "type": "INTEGER", "length": 4, "not_null": true }
]
}
```
(Trimmed: 4 internal-only columns omitted for clarity. The full
response includes 11 columns.)
### `cache_stats` ### `cache_stats`
Cache statistics: total entries, live entries, breakdown by method. Cache statistics: total entries, live entries, breakdown by method.