Updates for Documentation
This commit is contained in:
191
.claude/commands/ignition-configure.md
Normal file
191
.claude/commands/ignition-configure.md
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
# ignition-configure
|
||||||
|
|
||||||
|
Use this skill whenever the user asks you to configure, inspect, or manage the Ignition gateway
|
||||||
|
via the HTTP API — adding connections, importing tags, checking status, managing modules, etc.
|
||||||
|
|
||||||
|
## Setup: Resolve token and host
|
||||||
|
|
||||||
|
1. Read `docker/.env` for `GATEWAY_HOSTNAME` (default: `localhost:8088`)
|
||||||
|
2. Read `~/.config/ignition-dev/secrets.env` for `IGNITION_API_TOKEN` (format: `name:token`)
|
||||||
|
- Fall back to asking the user if not found
|
||||||
|
3. Set shell variables for all subsequent calls:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TOKEN=$(grep IGNITION_API_TOKEN ~/.config/ignition-dev/secrets.env | cut -d= -f2)
|
||||||
|
GW="http://$(grep GATEWAY_HOSTNAME docker/.env | cut -d= -f2- || echo 'localhost:8088')"
|
||||||
|
HEADER="X-Ignition-API-Token: $TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 1: Confirm gateway is reachable
|
||||||
|
|
||||||
|
Always check before making changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -H "$HEADER" $GW/data/api/v1/overview | python3 -m json.tool
|
||||||
|
```
|
||||||
|
|
||||||
|
If this fails, check `docker compose ps` and gateway logs before proceeding.
|
||||||
|
|
||||||
|
## Step 2: Inspect current state
|
||||||
|
|
||||||
|
Before creating or modifying anything, read what's already there:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Gateway connections overview
|
||||||
|
curl -s -H "$HEADER" $GW/data/api/v1/overview/connections | python3 -m json.tool
|
||||||
|
|
||||||
|
# Any critical problems
|
||||||
|
curl -s -H "$HEADER" $GW/data/api/v1/overview/problems | python3 -m json.tool
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Configuration Tasks
|
||||||
|
|
||||||
|
### Add a database connection
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# First — describe the schema to see available props
|
||||||
|
curl -s -H "$HEADER" $GW/data/api/v1/resources/type/ignition/database-connection | python3 -m json.tool
|
||||||
|
|
||||||
|
# Create
|
||||||
|
curl -s -X POST -H "$HEADER" -H "Content-Type: application/json" \
|
||||||
|
$GW/data/api/v1/resources/ignition/database-connection \
|
||||||
|
-d '{
|
||||||
|
"name": "ignition_db",
|
||||||
|
"enabled": true,
|
||||||
|
"props": {
|
||||||
|
"ConnectURL": "jdbc:postgresql://postgres:5432/ignition",
|
||||||
|
"Username": "ignition",
|
||||||
|
"Password": "'"$POSTGRES_PASSWORD"'"
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
curl -s -H "$HEADER" $GW/data/api/v1/resources/find/ignition/database-connection/ignition_db | python3 -m json.tool
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add an OPC connection (Modbus or AB PLC)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Describe schema first
|
||||||
|
curl -s -H "$HEADER" $GW/data/api/v1/resources/type/ignition/opc-connection | python3 -m json.tool
|
||||||
|
|
||||||
|
# Create
|
||||||
|
curl -s -X POST -H "$HEADER" -H "Content-Type: application/json" \
|
||||||
|
$GW/data/api/v1/resources/ignition/opc-connection \
|
||||||
|
-d '{
|
||||||
|
"name": "modbus-sim-pumps",
|
||||||
|
"enabled": true,
|
||||||
|
"props": {
|
||||||
|
"EndpointUrl": "opc.tcp://modbus-sim-pumps:4840",
|
||||||
|
"SecurityPolicy": "None",
|
||||||
|
"MessageSecurity": "None"
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add an OPC-UA device
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List available device types
|
||||||
|
curl -s -H "$HEADER" $GW/data/api/v1/resources/type/com.inductiveautomation.opcua/device | python3 -m json.tool
|
||||||
|
|
||||||
|
# Create
|
||||||
|
curl -s -X POST -H "$HEADER" -H "Content-Type: application/json" \
|
||||||
|
$GW/data/api/v1/resources/com.inductiveautomation.opcua/device \
|
||||||
|
-d '{ "name": "MyDevice", "enabled": true, "props": { ... } }'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modify an existing resource
|
||||||
|
|
||||||
|
Always GET first to obtain the current `signature`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Step 1: Get current config and signature
|
||||||
|
RESOURCE=$(curl -s -H "$HEADER" $GW/data/api/v1/resources/find/ignition/database-connection/ignition_db)
|
||||||
|
SIG=$(echo $RESOURCE | python3 -c "import json,sys; print(json.load(sys.stdin)['signature'])")
|
||||||
|
|
||||||
|
# Step 2: Modify (include signature in body)
|
||||||
|
curl -s -X PUT -H "$HEADER" -H "Content-Type: application/json" \
|
||||||
|
$GW/data/api/v1/resources/ignition/database-connection \
|
||||||
|
-d "{\"name\": \"ignition_db\", \"signature\": \"$SIG\", \"props\": { ... }}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Import tags
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Export first (to see current state or create a template)
|
||||||
|
curl -s -H "$HEADER" \
|
||||||
|
"$GW/data/api/v1/tags/export?provider=default&path=Devices&recursive=true" \
|
||||||
|
-o tags-export.json
|
||||||
|
|
||||||
|
# Import (collisionPolicy: Abort | Overwrite | Ignore | MergeOverwrite | MergeIgnore)
|
||||||
|
curl -s -X POST -H "$HEADER" -H "Content-Type: application/json" \
|
||||||
|
"$GW/data/api/v1/tags/import?provider=default&path=Devices&collisionPolicy=MergeOverwrite" \
|
||||||
|
-d @tags-export.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Download a backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -H "$HEADER" \
|
||||||
|
"$GW/data/api/v1/backup" \
|
||||||
|
-o "gateway-$(date +%Y%m%d-%H%M%S).gwbk"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## After Making Changes — Scan to Sync
|
||||||
|
|
||||||
|
After editing files in `ignition/project/`:
|
||||||
|
```bash
|
||||||
|
curl -s -X POST -H "$HEADER" $GW/data/api/v1/scan/projects
|
||||||
|
```
|
||||||
|
|
||||||
|
After editing files in `docker/gw-config/`:
|
||||||
|
```bash
|
||||||
|
curl -s -X POST -H "$HEADER" $GW/data/api/v1/scan/config
|
||||||
|
```
|
||||||
|
|
||||||
|
Check scan status:
|
||||||
|
```bash
|
||||||
|
curl -s -H "$HEADER" $GW/data/api/v1/scan/projects | python3 -m json.tool
|
||||||
|
curl -s -H "$HEADER" $GW/data/api/v1/scan/config | python3 -m json.tool
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gateway Restart ⚠️
|
||||||
|
|
||||||
|
**Never restart without explicit user confirmation.** Check for pending tasks first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check what requires a restart
|
||||||
|
curl -s -H "$HEADER" $GW/data/api/v1/restart-tasks/pending | python3 -m json.tool
|
||||||
|
|
||||||
|
# Restart (confirm=true required — disruptive to all active sessions)
|
||||||
|
curl -s -X POST -H "$HEADER" "$GW/data/api/v1/restart-tasks/restart?confirm=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Logs and Diagnostics
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Recent warnings and errors
|
||||||
|
curl -s -H "$HEADER" "$GW/data/api/v1/logs?minLevel=WARN&limit=50" | python3 -m json.tool
|
||||||
|
|
||||||
|
# Logs for a specific logger
|
||||||
|
curl -s -H "$HEADER" "$GW/data/api/v1/logs?logger=IgnitionGateway&limit=25" | python3 -m json.tool
|
||||||
|
|
||||||
|
# Audit trail
|
||||||
|
curl -s -H "$HEADER" "$GW/data/api/v1/audit/log/Audit?limit=25" | python3 -m json.tool
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
Full API reference: [ignition/ignition-api.md](../../ignition/ignition-api.md)
|
||||||
|
Live spec: `http://localhost:8088/openapi`
|
||||||
@@ -1,7 +1,20 @@
|
|||||||
{
|
{
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"Bash(find c:/Git/framework-ignition-docker/ignition -type f -name *)"
|
"Bash(find c:/Git/framework-ignition-docker/ignition -type f -name *)",
|
||||||
|
"WebFetch(domain:www.docs.inductiveautomation.com)",
|
||||||
|
"Bash(curl -s -H \"X-Ignition-API-Token: 0Pda7AWJeXENyHlvqi74ORZ20BeWtaDzk2L3WIsaUZs\" http://localhost:8088/openapi.json)",
|
||||||
|
"Bash(curl -sk -H \"X-Ignition-API-Token: Claude:0Pda7AWJeXENyHlvqi74ORZ20BeWtaDzk2L3WIsaUZs\" https://localhost:8043/openapi.json)",
|
||||||
|
"Bash(python3 -m json.tool)",
|
||||||
|
"Bash(curl -s -H \"X-Ignition-API-Token: Claude:0Pda7AWJeXENyHlvqi74ORZ20BeWtaDzk2L3WIsaUZs\" http://localhost:8088/openapi.json)",
|
||||||
|
"Bash(claude mcp:*)",
|
||||||
|
"Bash(command -v claude)",
|
||||||
|
"Read(//c/Users/b.peck/.local/bin/**)",
|
||||||
|
"Read(//usr/local/bin/**)",
|
||||||
|
"Bash(/c/Users/b.peck/.local/bin/claude mcp:*)",
|
||||||
|
"mcp__obsidian__obsidian_update_note",
|
||||||
|
"mcp__obsidian__obsidian_list_notes",
|
||||||
|
"mcp__obsidian__obsidian_read_note"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
77
CLAUDE.md
77
CLAUDE.md
@@ -13,43 +13,47 @@ The PLC I/O Testing Platform is the first project built on this framework.
|
|||||||
- Gitea at 192.168.3.59:3000 for source control
|
- Gitea at 192.168.3.59:3000 for source control
|
||||||
- Jython 2.7 inside Ignition; Python 3.10+ for external tooling
|
- Jython 2.7 inside Ignition; Python 3.10+ for external tooling
|
||||||
- Claude Code on the Linux host (VS Code Remote to Docker host)
|
- Claude Code on the Linux host (VS Code Remote to Docker host)
|
||||||
|
- WikiJS at wikijs.primecontrols-dev.com for team documentation (`.md` files auto-sync on `git push` via `tools/publish_docs.py`)
|
||||||
|
|
||||||
|
## Gateway HTTP API
|
||||||
|
|
||||||
|
Ignition 8.3 exposes a full REST API. Use it for infrastructure-as-code configuration —
|
||||||
|
adding DB connections, OPC connections, importing tags, managing projects, etc.
|
||||||
|
|
||||||
|
- **Browse**: `http://<gateway-host>:8088/openapi`
|
||||||
|
- **Spec**: `http://<gateway-host>:8088/openapi.json`
|
||||||
|
- **Auth header**: `X-Ignition-API-Token: <name>:<token>`
|
||||||
|
- **Full reference**: `ignition/ignition-api.md`
|
||||||
|
- **Skill**: `/ignition-configure` — resolves token and walks through config tasks
|
||||||
|
|
||||||
## API Key / Secrets Policy
|
## API Key / Secrets Policy
|
||||||
|
|
||||||
**Source of truth**: `~/.config/ignition-dev/secrets.env` (chmod 600, never committed)
|
**Source of truth**: `~/.config/ignition-dev/secrets.env` (chmod 600, never committed)
|
||||||
**Template**: `.env.example` in repo root — update this when adding new secrets
|
**Template**: `.env.example` in repo root — update this when adding new secrets
|
||||||
|
|
||||||
### Rules
|
|
||||||
- All secrets via environment variables — no hardcoded values in any file
|
- All secrets via environment variables — no hardcoded values in any file
|
||||||
- Docker Compose references host env vars: `VAR=${VAR}` pattern
|
- Docker Compose references host env vars: `VAR=${VAR}` pattern
|
||||||
- Python scripts use `python-dotenv` + `os.environ["KEY"]` (loud failure on missing)
|
- Python scripts use `python-dotenv` + `os.environ["KEY"]` (loud failure on missing)
|
||||||
- `.env` at project root is a symlink to secrets file — listed in .gitignore
|
- `.env` at project root is a symlink to secrets file — listed in .gitignore
|
||||||
- CI secrets live in Gitea repository secrets settings
|
- CI secrets live in Gitea repository secrets settings
|
||||||
|
|
||||||
### Required Variables
|
Key variables: `IGNITION_API_TOKEN`, `POSTGRES_PASSWORD` — see `.env.example` for full list.
|
||||||
See `.env.example` for full list.
|
|
||||||
|
|
||||||
## Language Rules
|
## Language Rules
|
||||||
|
|
||||||
### Jython 2.7 (Inside Ignition)
|
### Jython 2.7 (Inside Ignition)
|
||||||
|
|
||||||
All scripts in `ignition/project/` and gateway scripting run Jython 2.7.
|
All scripts in `ignition/project/` run Jython 2.7. Top violations:
|
||||||
Hard constraints — violating these causes runtime errors:
|
|
||||||
|
|
||||||
- NO f-strings → use `"text {}".format(val)` or `"text %s" % val`
|
- NO f-strings → use `"text {}".format(val)`
|
||||||
- NO walrus operator `:=`
|
- NO walrus operator `:=`
|
||||||
- NO `pathlib` → use `os.path`
|
|
||||||
- NO type hints → no `def foo(x: int) -> str:`
|
- NO type hints → no `def foo(x: int) -> str:`
|
||||||
- NO `dataclasses`, `enum.auto()`, `asyncio`
|
|
||||||
- NO dictionary unpacking `{**d1, **d2}` → use `d1.update(d2)`
|
Full constraint list: [ignition/CLAUDE.md](ignition/CLAUDE.md)
|
||||||
- NO `yield from` → use explicit loops
|
|
||||||
- Imports: `system.*` for Ignition API, standard Java/Jython libs only
|
|
||||||
|
|
||||||
### Python 3.10+ (External Tooling)
|
### Python 3.10+ (External Tooling)
|
||||||
|
|
||||||
All scripts outside Ignition (test runners, utilities, CI/CD) use Python 3.10+.
|
All scripts outside Ignition use Python 3.10+. Add at top of every file:
|
||||||
Comment the target environment at the top of every file:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# env: python3.10+ (external tooling — NOT Ignition)
|
# env: python3.10+ (external tooling — NOT Ignition)
|
||||||
@@ -63,6 +67,7 @@ Comment the target environment at the top of every file:
|
|||||||
4. **Never restart Ignition gateway** without explicit user confirmation.
|
4. **Never restart Ignition gateway** without explicit user confirmation.
|
||||||
5. **Never `docker compose down`** without confirmation (destroys unnamed volumes).
|
5. **Never `docker compose down`** without confirmation (destroys unnamed volumes).
|
||||||
6. **Every change needs a verification step** — scenario run, curl, tag read.
|
6. **Every change needs a verification step** — scenario run, curl, tag read.
|
||||||
|
7. **Scan after file edits** — after editing `ignition/project/` run `POST /data/api/v1/scan/projects`; after editing `docker/gw-config/` run `POST /data/api/v1/scan/config`. Use `/ignition-configure`.
|
||||||
|
|
||||||
## Canonical Patterns (Do Not Change Without Discussion)
|
## Canonical Patterns (Do Not Change Without Discussion)
|
||||||
|
|
||||||
@@ -73,24 +78,13 @@ Decouples simulation from PLC I/O structure. All device test tags follow this.
|
|||||||
|
|
||||||
### JSON Test Scenarios
|
### JSON Test Scenarios
|
||||||
|
|
||||||
Five required sections:
|
Five required sections: `identity`, `device`, `initial_state`, `sequence`, `expected_outcomes`.
|
||||||
- `identity` — scenario ID, version, description
|
|
||||||
- `device` — device type, UDT path, tag references
|
|
||||||
- `initial_state` — tag values to set before test begins
|
|
||||||
- `sequence` — ordered steps with `delay_ms` between actions
|
|
||||||
- `expected_outcomes` — pass/fail criteria with `tolerance`
|
|
||||||
|
|
||||||
See `testing/CLAUDE.md` for full schema.
|
See `testing/CLAUDE.md` for full schema.
|
||||||
|
|
||||||
### WebDev API
|
### WebDev API
|
||||||
|
|
||||||
All external I/O goes through WebDev endpoints — never direct tag file edits at runtime:
|
All external I/O goes through WebDev endpoints — never direct tag file edits at runtime:
|
||||||
- `tagWrite` — write values to tags
|
`tagWrite`, `tagRead`, `tagBrowse`, `health`. See `webdev/CLAUDE.md`.
|
||||||
- `tagRead` — read tag values
|
|
||||||
- `tagBrowse` — browse tag tree
|
|
||||||
- `health` — gateway and connection status
|
|
||||||
|
|
||||||
See `webdev/CLAUDE.md` for endpoint contracts.
|
|
||||||
|
|
||||||
### Ignition as I/O Orchestrator
|
### Ignition as I/O Orchestrator
|
||||||
|
|
||||||
@@ -102,17 +96,23 @@ Never bypass Ignition to write directly to Modbus or PLC.
|
|||||||
|
|
||||||
```
|
```
|
||||||
project-root/
|
project-root/
|
||||||
├── CLAUDE.md ← you are here
|
├── CLAUDE.md
|
||||||
|
├── scripts/ ← setup.sh and utility scripts
|
||||||
├── docker/
|
├── docker/
|
||||||
│ ├── CLAUDE.md ← Docker Compose patterns
|
│ ├── CLAUDE.md
|
||||||
│ ├── docker-compose.yml
|
│ ├── docker-compose.yml
|
||||||
│ ├── docker-compose.override.yml
|
│ ├── docker-compose.override.yml
|
||||||
|
│ ├── gw-build/ ← custom Ignition Dockerfile
|
||||||
|
│ ├── gw-init/ ← gateway env vars (no secrets)
|
||||||
|
│ ├── gw-commission/ ← commissioning.json
|
||||||
|
│ ├── gw-config/ ← gateway runtime config (bind-mounted; runtime dirs gitignored)
|
||||||
|
│ ├── gw-secret/ ← admin password file (gitignored)
|
||||||
│ └── config/
|
│ └── config/
|
||||||
│ ├── ignition/
|
│ ├── postgres/ ← init.sql
|
||||||
│ ├── postgres/
|
│ └── traefik/ ← traefik.yml, dynamic/
|
||||||
│ └── traefik/
|
|
||||||
├── ignition/
|
├── ignition/
|
||||||
│ ├── CLAUDE.md ← Ignition project structure
|
│ ├── CLAUDE.md
|
||||||
|
│ ├── ignition-api.md ← Gateway HTTP API reference
|
||||||
│ └── project/
|
│ └── project/
|
||||||
│ ├── com.inductiveautomation.perspective/
|
│ ├── com.inductiveautomation.perspective/
|
||||||
│ ├── com.inductiveautomation.webdev/
|
│ ├── com.inductiveautomation.webdev/
|
||||||
@@ -121,23 +121,14 @@ project-root/
|
|||||||
│ ├── script-python/
|
│ ├── script-python/
|
||||||
│ └── global-props/
|
│ └── global-props/
|
||||||
├── testing/
|
├── testing/
|
||||||
│ ├── CLAUDE.md ← test scenario schema & runner
|
│ ├── CLAUDE.md
|
||||||
│ ├── scenarios/
|
│ ├── scenarios/
|
||||||
│ ├── runner/
|
│ ├── runner/
|
||||||
│ └── results/
|
│ └── results/
|
||||||
├── webdev/
|
├── webdev/
|
||||||
│ ├── CLAUDE.md ← WebDev API contract
|
│ ├── CLAUDE.md
|
||||||
│ └── endpoints/
|
│ └── endpoints/
|
||||||
├── tools/
|
├── tools/
|
||||||
│ └── (Python 3.10+ external utilities)
|
│ └── (Python 3.10+ external utilities)
|
||||||
└── docs/
|
└── docs/
|
||||||
└── (project documentation)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Module CLAUDE.md Files
|
|
||||||
|
|
||||||
Each subdirectory has its own CLAUDE.md with domain-specific rules:
|
|
||||||
- `docker/CLAUDE.md` — container orchestration, volumes, networking
|
|
||||||
- `ignition/CLAUDE.md` — project structure, UDTs, scripting, resources
|
|
||||||
- `testing/CLAUDE.md` — scenario schema, test runner, results format
|
|
||||||
- `webdev/CLAUDE.md` — API endpoint contracts, request/response formats
|
|
||||||
29
README.md
29
README.md
@@ -21,6 +21,16 @@ Reusable scaffold for Ignition SCADA + Docker projects, designed to work with Cl
|
|||||||
- Docker + Docker Compose installed on the host (Ubuntu 24 recommended)
|
- Docker + Docker Compose installed on the host (Ubuntu 24 recommended)
|
||||||
- Git repo cloned to the host machine
|
- Git repo cloned to the host machine
|
||||||
|
|
||||||
|
### Automated (recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The script creates the `proxy` network, sets up `~/.config/ignition-dev/secrets.env`, symlinks `docker/.env` to it, and scaffolds the gateway password file. Edit those two files, then jump to step 4 below.
|
||||||
|
|
||||||
|
### Manual
|
||||||
|
|
||||||
### 1. Create the secrets file
|
### 1. Create the secrets file
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -47,7 +57,17 @@ Key variables in `.env`:
|
|||||||
| `GATEWAY_HOSTNAME` | `ignition.localhost` | Public hostname (Traefik routing) |
|
| `GATEWAY_HOSTNAME` | `ignition.localhost` | Public hostname (Traefik routing) |
|
||||||
| `POSTGRES_PASSWORD` | `changeme` | PostgreSQL password |
|
| `POSTGRES_PASSWORD` | `changeme` | PostgreSQL password |
|
||||||
|
|
||||||
### 3. Build and start the stack
|
### 3. Create the external Docker network
|
||||||
|
|
||||||
|
The stack uses an external `proxy` network (shared across projects). Create it once per Docker host:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker network create proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a no-op if it already exists.
|
||||||
|
|
||||||
|
### 4. Build and start the stack
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd docker
|
cd docker
|
||||||
@@ -55,14 +75,14 @@ docker compose build # builds the custom Ignition image
|
|||||||
docker compose up -d # starts all services in background
|
docker compose up -d # starts all services in background
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Verify services are healthy
|
### 5. Verify services are healthy
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose ps # all services should show "healthy" or "running"
|
docker compose ps # all services should show "healthy" or "running"
|
||||||
curl -s http://localhost:8088/StatusPing # should return "RUNNING"
|
curl -s http://localhost:8088/StatusPing # should return "RUNNING"
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. Open the gateway
|
### 6. Open the gateway
|
||||||
|
|
||||||
- Ignition gateway: http://localhost:8088
|
- Ignition gateway: http://localhost:8088
|
||||||
- Traefik dashboard: http://localhost:8080
|
- Traefik dashboard: http://localhost:8080
|
||||||
@@ -71,7 +91,7 @@ curl -s http://localhost:8088/StatusPing # should return "RUNNING"
|
|||||||
> **Note:** On first boot the gateway skips the setup wizard (`commissioning.json` pre-commissions it).
|
> **Note:** On first boot the gateway skips the setup wizard (`commissioning.json` pre-commissions it).
|
||||||
> Log in with username `admin` and the password from `gw-secret/GATEWAY_ADMIN_PASSWORD`.
|
> Log in with username `admin` and the password from `gw-secret/GATEWAY_ADMIN_PASSWORD`.
|
||||||
|
|
||||||
### 6. Connect Ignition Designer
|
### 7. Connect Ignition Designer
|
||||||
|
|
||||||
Open Ignition Designer Launcher, add gateway at `http://<host-ip>:8088`, and open the **Framework** project. The project files in `ignition/project/` are live-mounted — changes saved in Designer write directly to Git-tracked files.
|
Open Ignition Designer Launcher, add gateway at `http://<host-ip>:8088`, and open the **Framework** project. The project files in `ignition/project/` are live-mounted — changes saved in Designer write directly to Git-tracked files.
|
||||||
|
|
||||||
@@ -85,6 +105,7 @@ See [CLAUDE.md](CLAUDE.md) for project directives, canonical patterns, and direc
|
|||||||
|
|
||||||
```
|
```
|
||||||
├── CLAUDE.md ← root directives (Claude Code reads this automatically)
|
├── CLAUDE.md ← root directives (Claude Code reads this automatically)
|
||||||
|
├── scripts/ ← setup and utility scripts
|
||||||
├── docker/ ← Docker Compose stack and configs
|
├── docker/ ← Docker Compose stack and configs
|
||||||
├── ignition/ ← Ignition project (mounted into container)
|
├── ignition/ ← Ignition project (mounted into container)
|
||||||
├── testing/ ← test scenarios, runner, results
|
├── testing/ ← test scenarios, runner, results
|
||||||
|
|||||||
@@ -15,5 +15,11 @@ providers:
|
|||||||
directory: /etc/traefik/dynamic
|
directory: /etc/traefik/dynamic
|
||||||
watch: true
|
watch: true
|
||||||
|
|
||||||
|
# Suppress encoded-character split-view warning (Ignition is RFC 3986 compliant)
|
||||||
|
core:
|
||||||
|
defaultRuleSyntax: v3
|
||||||
|
|
||||||
log:
|
log:
|
||||||
level: INFO
|
level: INFO
|
||||||
|
|
||||||
|
ping: {}
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ignition-data:/usr/local/bin/ignition/data
|
- ignition-data:/usr/local/bin/ignition/data
|
||||||
- ./gw-config:/usr/local/bin/ignition/data/config:rw
|
- ./gw-config:/usr/local/bin/ignition/data/config:rw
|
||||||
- ./gw-commission/commissioning.json:/usr/local/bin/ignition/data/commissioning.json:ro
|
# commissioning.json disabled — EULA hash is version-specific; let env vars handle it
|
||||||
|
# - ./gw-commission/commissioning.json:/usr/local/bin/ignition/data/commissioning.json:ro
|
||||||
- ../ignition/project:/usr/local/bin/ignition/data/projects/framework:rw
|
- ../ignition/project:/usr/local/bin/ignition/data/projects/framework:rw
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8088/StatusPing"]
|
test: ["CMD", "curl", "-f", "http://localhost:8088/StatusPing"]
|
||||||
@@ -108,6 +109,7 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
proxy:
|
proxy:
|
||||||
name: proxy
|
name: proxy
|
||||||
|
external: true
|
||||||
|
|
||||||
secrets:
|
secrets:
|
||||||
gateway-admin-password:
|
gateway-admin-password:
|
||||||
|
|||||||
@@ -108,6 +108,19 @@ Changing the PLC program only requires updating `LinkPath` and `LinkData`.
|
|||||||
|
|
||||||
## Scripting
|
## Scripting
|
||||||
|
|
||||||
|
### Jython 2.7 Constraints
|
||||||
|
|
||||||
|
All scripts in `ignition/project/` run Jython 2.7. Violating these causes runtime errors:
|
||||||
|
|
||||||
|
- NO f-strings → use `"text {}".format(val)` or `"text %s" % val`
|
||||||
|
- NO walrus operator `:=`
|
||||||
|
- NO `pathlib` → use `os.path`
|
||||||
|
- NO type hints → no `def foo(x: int) -> str:`
|
||||||
|
- NO `dataclasses`, `enum.auto()`, `asyncio`
|
||||||
|
- NO dictionary unpacking `{**d1, **d2}` → use `d1.update(d2)`
|
||||||
|
- NO `yield from` → use explicit loops
|
||||||
|
- Imports: `system.*` for Ignition API, standard Java/Jython libs only
|
||||||
|
|
||||||
### Project Library Scripts (`script-python/`)
|
### Project Library Scripts (`script-python/`)
|
||||||
|
|
||||||
Jython 2.7 — see root CLAUDE.md for language constraints.
|
Jython 2.7 — see root CLAUDE.md for language constraints.
|
||||||
|
|||||||
434
ignition/ignition-api.md
Normal file
434
ignition/ignition-api.md
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
# Ignition Gateway HTTP API — Reference
|
||||||
|
|
||||||
|
Parent: [../CLAUDE.md](../CLAUDE.md)
|
||||||
|
|
||||||
|
Live spec: `http://<gateway-host>:8088/openapi` (UI) · `http://<gateway-host>:8088/openapi.json` (JSON)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
All requests require an API token in a custom header:
|
||||||
|
|
||||||
|
```
|
||||||
|
X-Ignition-API-Token: <name>:<token>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `<name>` is the token's display name in the gateway. Tokens are managed at
|
||||||
|
**Platform > Security > API Keys** in the gateway web UI, or via the config API.
|
||||||
|
|
||||||
|
- **GET requests** are not recorded in the audit log.
|
||||||
|
- **POST / PUT / DELETE** are recorded in the audit log — use with intent.
|
||||||
|
- Store tokens in `~/.config/ignition-dev/secrets.env` as `IGNITION_API_TOKEN=<name>:<token>`.
|
||||||
|
Never hardcode tokens in scripts or commit them.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TOKEN="Claude:0Pda7AWJeXENyHlvqi74ORZ20BeWtaDzk2L3WIsaUZs"
|
||||||
|
GW="http://localhost:8088"
|
||||||
|
curl -s -H "X-Ignition-API-Token: $TOKEN" $GW/data/api/v1/overview
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gateway Status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# General status — uptime, edition, state
|
||||||
|
GET /data/api/v1/overview
|
||||||
|
|
||||||
|
# All connections (DB, OPC, device)
|
||||||
|
GET /data/api/v1/overview/connections
|
||||||
|
|
||||||
|
# Critical problems blocking normal operation
|
||||||
|
GET /data/api/v1/overview/problems
|
||||||
|
|
||||||
|
# Gateway name only
|
||||||
|
GET /data/api/v1/overview/name
|
||||||
|
|
||||||
|
# Detailed gateway info
|
||||||
|
GET /data/api/v1/gateway-info
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scan / Filesystem Sync
|
||||||
|
|
||||||
|
Ignition watches mounted directories for changes, but an explicit scan guarantees
|
||||||
|
immediate pickup. **Always scan after editing project or config files.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Trigger project scan — run after editing ignition/project/ files
|
||||||
|
POST /data/api/v1/scan/projects
|
||||||
|
|
||||||
|
# Check project scan status
|
||||||
|
GET /data/api/v1/scan/projects
|
||||||
|
|
||||||
|
# Trigger config scan — run after editing gw-config/ files
|
||||||
|
POST /data/api/v1/scan/config
|
||||||
|
|
||||||
|
# Check config scan status
|
||||||
|
GET /data/api/v1/scan/config
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scan Lock (bulk operations)
|
||||||
|
|
||||||
|
Acquire a scan lock before making multiple changes to prevent partial-state scans.
|
||||||
|
The lock releases automatically when the POST scan is triggered.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Acquire lock (body: {"timeoutSeconds": 60})
|
||||||
|
POST /data/api/v1/scan-lock/projects
|
||||||
|
POST /data/api/v1/scan-lock/config
|
||||||
|
|
||||||
|
# Check current lock holder
|
||||||
|
GET /data/api/v1/scan-lock/projects
|
||||||
|
GET /data/api/v1/scan-lock/config
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resource API Pattern
|
||||||
|
|
||||||
|
Almost all gateway configuration uses a common CRUD pattern:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /data/api/v1/resources/list/{moduleId}/{typeId} list all (verbose)
|
||||||
|
GET /data/api/v1/resources/names/{moduleId}/{typeId} names + enabled status
|
||||||
|
GET /data/api/v1/resources/find/{moduleId}/{typeId}/{name} get one + signature
|
||||||
|
GET /data/api/v1/resources/type/{moduleId}/{typeId} describe schema
|
||||||
|
POST /data/api/v1/resources/{moduleId}/{typeId} create
|
||||||
|
PUT /data/api/v1/resources/{moduleId}/{typeId} modify (requires signature)
|
||||||
|
DELETE /data/api/v1/resources/{moduleId}/{typeId}/{name}/{sig} delete
|
||||||
|
POST /data/api/v1/resources/rename/{moduleId}/{typeId}/{name} rename
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Always GET before PUT/DELETE** — the `signature` field from a GET response is required
|
||||||
|
> for modify and delete operations. It changes whenever the resource is updated.
|
||||||
|
|
||||||
|
### Key moduleId / typeId pairs
|
||||||
|
|
||||||
|
| Resource | moduleId | typeId |
|
||||||
|
|---|---|---|
|
||||||
|
| Database connection | `ignition` | `database-connection` |
|
||||||
|
| OPC connection | `ignition` | `opc-connection` |
|
||||||
|
| Tag provider | `ignition` | `tag-provider` |
|
||||||
|
| OPC-UA device | `com.inductiveautomation.opcua` | `device` |
|
||||||
|
| OPC-UA server config | `com.inductiveautomation.opcua` | `server-config` |
|
||||||
|
| OPC-UA access control | `com.inductiveautomation.opcua` | `access-control` |
|
||||||
|
|
||||||
|
### Common query parameters
|
||||||
|
|
||||||
|
| Param | Example | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `limit` | `25` | Max items to return |
|
||||||
|
| `offset` | `50` | Items to skip |
|
||||||
|
| `sortBy` | `asc(name)` | Sort field + direction |
|
||||||
|
| `search` | `postgres` | Free-text filter |
|
||||||
|
| `filter[field[op]]` | `filter[enabled[eq]]=true` | Field filter |
|
||||||
|
|
||||||
|
Filter operators: `eq` `ne` `cn` `sw` `ew` `gt` `gte` `lt` `lte` `rgx`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database Connections
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all database connections
|
||||||
|
GET /data/api/v1/resources/list/ignition/database-connection
|
||||||
|
|
||||||
|
# Get one (returns config + signature)
|
||||||
|
GET /data/api/v1/resources/find/ignition/database-connection/{name}
|
||||||
|
|
||||||
|
# Create a PostgreSQL connection
|
||||||
|
POST /data/api/v1/resources/ignition/database-connection
|
||||||
|
Content-Type: application/json
|
||||||
|
{
|
||||||
|
"name": "ignition_db",
|
||||||
|
"enabled": true,
|
||||||
|
"props": {
|
||||||
|
"ConnectURL": "jdbc:postgresql://postgres:5432/ignition",
|
||||||
|
"Username": "ignition",
|
||||||
|
"Password": "<password>",
|
||||||
|
"ValidateOnCheckout": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Modify (requires signature from GET)
|
||||||
|
PUT /data/api/v1/resources/ignition/database-connection
|
||||||
|
{ "name": "ignition_db", "signature": "<sig>", "props": { ... } }
|
||||||
|
|
||||||
|
# Delete
|
||||||
|
DELETE /data/api/v1/resources/ignition/database-connection/{name}/{signature}
|
||||||
|
|
||||||
|
# Describe schema (shows all available props)
|
||||||
|
GET /data/api/v1/resources/type/ignition/database-connection
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## OPC Connections
|
||||||
|
|
||||||
|
Used to connect Ignition to Modbus simulators and AB PLC controllers via OPC-UA.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all OPC connections
|
||||||
|
GET /data/api/v1/resources/list/ignition/opc-connection
|
||||||
|
|
||||||
|
# Get one
|
||||||
|
GET /data/api/v1/resources/find/ignition/opc-connection/{name}
|
||||||
|
|
||||||
|
# Create an OPC-UA connection to a Modbus sim
|
||||||
|
POST /data/api/v1/resources/ignition/opc-connection
|
||||||
|
{
|
||||||
|
"name": "modbus-sim-pumps",
|
||||||
|
"enabled": true,
|
||||||
|
"props": {
|
||||||
|
"EndpointUrl": "opc.tcp://modbus-sim-pumps:4840",
|
||||||
|
"SecurityPolicy": "None",
|
||||||
|
"MessageSecurity": "None"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Describe schema
|
||||||
|
GET /data/api/v1/resources/type/ignition/opc-connection
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## OPC-UA Devices
|
||||||
|
|
||||||
|
OPC-UA device driver instances (Allen-Bradley, Modbus TCP, etc.).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all devices
|
||||||
|
GET /data/api/v1/resources/list/com.inductiveautomation.opcua/device
|
||||||
|
|
||||||
|
# Get one
|
||||||
|
GET /data/api/v1/resources/find/com.inductiveautomation.opcua/device/{name}
|
||||||
|
|
||||||
|
# Describe available device types and their properties
|
||||||
|
GET /data/api/v1/resources/type/com.inductiveautomation.opcua/device
|
||||||
|
|
||||||
|
# Create (body varies by device type — use type endpoint to discover props)
|
||||||
|
POST /data/api/v1/resources/com.inductiveautomation.opcua/device
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tag Providers
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List tag providers
|
||||||
|
GET /data/api/v1/resources/list/ignition/tag-provider
|
||||||
|
|
||||||
|
# Get one
|
||||||
|
GET /data/api/v1/resources/find/ignition/tag-provider/{name}
|
||||||
|
|
||||||
|
# Describe schema
|
||||||
|
GET /data/api/v1/resources/type/ignition/tag-provider
|
||||||
|
|
||||||
|
# Create / modify follow the standard resource pattern
|
||||||
|
POST /data/api/v1/resources/ignition/tag-provider
|
||||||
|
PUT /data/api/v1/resources/ignition/tag-provider
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tag Import / Export
|
||||||
|
|
||||||
|
Primary mechanism for bulk tag management outside of Designer.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Export tags as JSON (default), XML, or CSV
|
||||||
|
GET /data/api/v1/tags/export?provider=default&path=Devices&recursive=true&type=json
|
||||||
|
# Save to file:
|
||||||
|
curl -s -H "X-Ignition-API-Token: $TOKEN" \
|
||||||
|
"$GW/data/api/v1/tags/export?provider=default&path=Devices&recursive=true" \
|
||||||
|
-o tags-export.json
|
||||||
|
|
||||||
|
# Import tags
|
||||||
|
# collisionPolicy: Abort | Overwrite | Ignore | MergeOverwrite | MergeIgnore
|
||||||
|
POST /data/api/v1/tags/import?provider=default&path=Devices&collisionPolicy=Overwrite
|
||||||
|
Content-Type: application/json
|
||||||
|
<tag export JSON body>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Projects
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all projects
|
||||||
|
GET /data/api/v1/projects/list
|
||||||
|
|
||||||
|
# Get project details
|
||||||
|
GET /data/api/v1/projects/find/{name}
|
||||||
|
|
||||||
|
# Create a project
|
||||||
|
POST /data/api/v1/projects
|
||||||
|
{ "name": "my-project", "title": "My Project", "enabled": true }
|
||||||
|
|
||||||
|
# Modify a project
|
||||||
|
PUT /data/api/v1/projects/{name}
|
||||||
|
|
||||||
|
# Export project as zip archive
|
||||||
|
GET /data/api/v1/projects/export/{name}
|
||||||
|
|
||||||
|
# Import project from zip
|
||||||
|
POST /data/api/v1/projects/import/{name}?overwrite=true
|
||||||
|
Content-Type: application/octet-stream
|
||||||
|
<zip file body>
|
||||||
|
|
||||||
|
# Delete project
|
||||||
|
DELETE /data/api/v1/projects/{name}?confirm=true
|
||||||
|
```
|
||||||
|
|
||||||
|
After creating or modifying projects via file edits, trigger a scan:
|
||||||
|
```bash
|
||||||
|
curl -s -X POST -H "X-Ignition-API-Token: $TOKEN" $GW/data/api/v1/scan/projects
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Perspective Sessions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all active Perspective sessions
|
||||||
|
GET /data/perspective/api/v1/sessions/
|
||||||
|
|
||||||
|
# Get session details
|
||||||
|
GET /data/perspective/api/v1/session/{sessionId}
|
||||||
|
|
||||||
|
# List pages in a session
|
||||||
|
GET /data/perspective/api/v1/session/{sessionId}/pages
|
||||||
|
|
||||||
|
# List views on a page
|
||||||
|
GET /data/perspective/api/v1/session/{sessionId}/page/{pageId}/views
|
||||||
|
|
||||||
|
# Terminate session(s)
|
||||||
|
DELETE /data/perspective/api/v1/sessions?sessionId={id}&message=Maintenance
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modules
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all healthy (non-quarantined) modules
|
||||||
|
GET /data/api/v1/modules/healthy
|
||||||
|
|
||||||
|
# List quarantined modules
|
||||||
|
GET /data/api/v1/modules/quarantined
|
||||||
|
|
||||||
|
# Enable or disable a module
|
||||||
|
PUT /data/api/v1/modules/toggle-state
|
||||||
|
{ "moduleId": "com.inductiveautomation.webdev", "enabled": true }
|
||||||
|
|
||||||
|
# Upload a module file (.modl)
|
||||||
|
POST /data/api/v1/modules/upload?fileName=MyModule.modl
|
||||||
|
Content-Type: application/octet-stream
|
||||||
|
|
||||||
|
# Install after upload
|
||||||
|
POST /data/api/v1/modules/install?moduleId=com.example.mymodule
|
||||||
|
|
||||||
|
# Accept EULA
|
||||||
|
POST /data/api/v1/modules/eula?moduleId=com.example.mymodule
|
||||||
|
|
||||||
|
# Uninstall (takes effect after restart)
|
||||||
|
DELETE /data/api/v1/modules/uninstall
|
||||||
|
{ "moduleIds": ["com.example.mymodule"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Query gateway logs
|
||||||
|
GET /data/api/v1/logs?minLevel=WARN&limit=100
|
||||||
|
|
||||||
|
# Filter by logger name
|
||||||
|
GET /data/api/v1/logs?logger=IgnitionGateway&limit=50
|
||||||
|
|
||||||
|
# Filter by time range (ISO 8601)
|
||||||
|
GET /data/api/v1/logs?startTime=2026-03-17T00:00:00Z&endTime=2026-03-17T23:59:59Z
|
||||||
|
|
||||||
|
# List all loggers and their current levels
|
||||||
|
GET /data/api/v1/logs/loggers
|
||||||
|
|
||||||
|
# Set a logger level temporarily
|
||||||
|
POST /data/api/v1/logs/loggers/{loggerName}?level=DEBUG
|
||||||
|
|
||||||
|
# Download full log file
|
||||||
|
GET /data/api/v1/logs/download
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Audit Log
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Query audit events for a profile (default profile name: "Audit")
|
||||||
|
GET /data/api/v1/audit/log/Audit?limit=50
|
||||||
|
|
||||||
|
# Filter by actor
|
||||||
|
GET /data/api/v1/audit/log/Audit?actorFilter=admin
|
||||||
|
|
||||||
|
# Filter by action
|
||||||
|
GET /data/api/v1/audit/log/Audit?actionFilter=Created
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gateway Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Download a .gwbk backup file
|
||||||
|
curl -s -H "X-Ignition-API-Token: $TOKEN" \
|
||||||
|
"$GW/data/api/v1/backup" \
|
||||||
|
-o "gateway-$(date +%Y%m%d-%H%M%S).gwbk"
|
||||||
|
|
||||||
|
# Restore from backup (gateway will restart)
|
||||||
|
POST /data/api/v1/backup?restoreDisabled=false
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
<gwbk file>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gateway Restart ⚠️
|
||||||
|
|
||||||
|
**Disruptive — all sessions will be terminated. Get explicit user confirmation first.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if a restart is needed (pending tasks)
|
||||||
|
GET /data/api/v1/restart-tasks/pending
|
||||||
|
|
||||||
|
# Restart — confirm=true is required
|
||||||
|
curl -s -X POST -H "X-Ignition-API-Token: $TOKEN" \
|
||||||
|
"$GW/data/api/v1/restart-tasks/restart?confirm=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
Per `CLAUDE.md` safety rules: never restart the gateway without explicit user confirmation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Token Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate a new key/hash pair (store the key — it is not retrievable again)
|
||||||
|
POST /data/api/v1/api-token/generate
|
||||||
|
|
||||||
|
# List existing API token configs
|
||||||
|
GET /data/api/v1/resources/list/ignition/api-token
|
||||||
|
|
||||||
|
# Create an API token resource
|
||||||
|
POST /data/api/v1/resources/ignition/api-token
|
||||||
|
{
|
||||||
|
"name": "MyToken",
|
||||||
|
"props": {
|
||||||
|
"Enabled": true,
|
||||||
|
"RateLimit": -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
31
scripts/install-hooks.sh
Normal file
31
scripts/install-hooks.sh
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# install-hooks.sh — install git hooks for this repo.
|
||||||
|
# Called automatically by setup.sh; safe to re-run.
|
||||||
|
# Usage: bash scripts/install-hooks.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
HOOKS_DIR="$REPO_ROOT/.git/hooks"
|
||||||
|
|
||||||
|
if [[ ! -d "$HOOKS_DIR" ]]; then
|
||||||
|
echo "ERROR: .git/hooks directory not found. Are you in a git repo?"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── post-push: sync markdown docs to WikiJS ───────────────────────────────────
|
||||||
|
POST_PUSH="$HOOKS_DIR/post-push"
|
||||||
|
|
||||||
|
cat > "$POST_PUSH" << 'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# post-push hook — sync markdown files to WikiJS after every push.
|
||||||
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
if command -v python3 &>/dev/null; then
|
||||||
|
python3 tools/publish_docs.py || echo "[wikijs] publish failed — run manually: python3 tools/publish_docs.py"
|
||||||
|
else
|
||||||
|
echo "[wikijs] python3 not found — skipping doc sync"
|
||||||
|
fi
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod +x "$POST_PUSH"
|
||||||
|
echo " Installed post-push hook → $POST_PUSH"
|
||||||
81
scripts/setup.sh
Normal file
81
scripts/setup.sh
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# setup.sh — first-time environment bootstrap for the Ignition + Docker framework.
|
||||||
|
# Run once after cloning. Safe to re-run (all steps are idempotent).
|
||||||
|
# Usage: bash scripts/setup.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
DOCKER_DIR="$REPO_ROOT/docker"
|
||||||
|
SECRETS_DIR="$HOME/.config/ignition-dev"
|
||||||
|
SECRETS_FILE="$SECRETS_DIR/secrets.env"
|
||||||
|
|
||||||
|
echo "=== Ignition + Docker Framework — First-Time Setup ==="
|
||||||
|
|
||||||
|
# ── 1. Create the external proxy network ────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "[1/4] Docker proxy network..."
|
||||||
|
if docker network inspect proxy &>/dev/null; then
|
||||||
|
echo " proxy network already exists — skipping."
|
||||||
|
else
|
||||||
|
docker network create proxy
|
||||||
|
echo " Created proxy network."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 2. Secrets file at ~/.config/ignition-dev/secrets.env ───────────────────
|
||||||
|
echo ""
|
||||||
|
echo "[2/4] Secrets file..."
|
||||||
|
if [[ -f "$SECRETS_FILE" ]]; then
|
||||||
|
echo " $SECRETS_FILE already exists — skipping."
|
||||||
|
else
|
||||||
|
mkdir -p "$SECRETS_DIR"
|
||||||
|
cp "$DOCKER_DIR/.env.example" "$SECRETS_FILE"
|
||||||
|
chmod 600 "$SECRETS_FILE"
|
||||||
|
echo " Created $SECRETS_FILE (chmod 600)."
|
||||||
|
echo " ACTION REQUIRED: Edit $SECRETS_FILE and set real passwords."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 3. Symlink docker/.env → secrets file ───────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "[3/4] docker/.env symlink..."
|
||||||
|
ENV_LINK="$DOCKER_DIR/.env"
|
||||||
|
if [[ -L "$ENV_LINK" && "$(readlink "$ENV_LINK")" == "$SECRETS_FILE" ]]; then
|
||||||
|
echo " Symlink already correct — skipping."
|
||||||
|
elif [[ -f "$ENV_LINK" && ! -L "$ENV_LINK" ]]; then
|
||||||
|
echo " WARNING: $ENV_LINK exists as a regular file, not a symlink."
|
||||||
|
echo " It will not be replaced automatically. Inspect and remove it if safe."
|
||||||
|
else
|
||||||
|
ln -sf "$SECRETS_FILE" "$ENV_LINK"
|
||||||
|
echo " Linked docker/.env → $SECRETS_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 4. Gateway admin password file ──────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "[4/5] Gateway admin password..."
|
||||||
|
PW_FILE="$DOCKER_DIR/gw-secret/GATEWAY_ADMIN_PASSWORD"
|
||||||
|
PW_EXAMPLE="$DOCKER_DIR/gw-secret/GATEWAY_ADMIN_PASSWORD.example"
|
||||||
|
if [[ -f "$PW_FILE" && "$(cat "$PW_FILE")" != "changeme" ]]; then
|
||||||
|
echo " Password file already set — skipping."
|
||||||
|
else
|
||||||
|
if [[ ! -f "$PW_FILE" ]]; then
|
||||||
|
cp "$PW_EXAMPLE" "$PW_FILE"
|
||||||
|
echo " Created $PW_FILE from example."
|
||||||
|
fi
|
||||||
|
echo " ACTION REQUIRED: Edit docker/gw-secret/GATEWAY_ADMIN_PASSWORD and set a real password."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 5. Git hooks ─────────────────────────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "[5/5] Git hooks..."
|
||||||
|
bash "$REPO_ROOT/scripts/install-hooks.sh"
|
||||||
|
|
||||||
|
# ── Done ─────────────────────────────────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "=== Setup complete ==="
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Edit $SECRETS_FILE (set passwords, WIKIJS_API_KEY)"
|
||||||
|
echo " 2. Edit docker/gw-secret/GATEWAY_ADMIN_PASSWORD (gateway admin password)"
|
||||||
|
echo " 3. cd docker && docker compose build && docker compose up -d"
|
||||||
|
echo " 4. curl -s http://localhost:8088/StatusPing # should return RUNNING"
|
||||||
|
echo " 5. python3 tools/publish_docs.py --dry-run # verify WikiJS doc sync"
|
||||||
200
tools/publish_docs.py
Normal file
200
tools/publish_docs.py
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
# env: python3.10+ (external tooling — NOT Ignition)
|
||||||
|
"""
|
||||||
|
publish_docs.py — sync selected markdown files to WikiJS via GraphQL API.
|
||||||
|
|
||||||
|
Reads WIKIJS_API_KEY and WIKIJS_URL from the environment (or docker/.env via python-dotenv).
|
||||||
|
Run manually or via the git post-push hook installed by scripts/install-hooks.sh.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 tools/publish_docs.py # sync all files
|
||||||
|
python3 tools/publish_docs.py --dry-run # preview only, no API calls
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# ── Config ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).parent.parent
|
||||||
|
|
||||||
|
# Load secrets from docker/.env (symlink to ~/.config/ignition-dev/secrets.env)
|
||||||
|
load_dotenv(REPO_ROOT / "docker" / ".env")
|
||||||
|
|
||||||
|
WIKIJS_URL = os.environ.get("WIKIJS_URL", "https://wikijs.primecontrols-dev.com")
|
||||||
|
WIKIJS_API_KEY = os.environ["WIKIJS_API_KEY"] # loud failure if missing
|
||||||
|
|
||||||
|
GRAPHQL_ENDPOINT = f"{WIKIJS_URL.rstrip('/')}/graphql"
|
||||||
|
LOCALE = "en"
|
||||||
|
|
||||||
|
# Maps repo-relative file path → WikiJS page path (no leading slash)
|
||||||
|
MANIFEST: dict[str, str] = {
|
||||||
|
"README.md": "en/engineering/AI-Framework/home",
|
||||||
|
"CLAUDE.md": "en/engineering/AI-Framework/CLAUDE",
|
||||||
|
"docker/CLAUDE.md": "en/engineering/AI-Framework/docker",
|
||||||
|
"ignition/CLAUDE.md": "en/engineering/AI-Framework/ignition",
|
||||||
|
"testing/CLAUDE.md": "en/engineering/AI-Framework/testing",
|
||||||
|
"webdev/CLAUDE.md": "en/engineering/AI-Framework/webdev",
|
||||||
|
"ignition/ignition-api.md": "en/engineering/AI-Framework/ignition/ignition-api",
|
||||||
|
"docker/config/traefik/dynamic/README.md": "en/engineering/AI-Framework/docker/traefik",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def gql(query: str, variables: dict | None = None) -> dict:
|
||||||
|
resp = requests.post(
|
||||||
|
GRAPHQL_ENDPOINT,
|
||||||
|
json={"query": query, "variables": variables or {}},
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {WIKIJS_API_KEY}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
if "errors" in data:
|
||||||
|
raise RuntimeError(f"GraphQL errors: {data['errors']}")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def page_id_for_path(wiki_path: str) -> int | None:
|
||||||
|
"""Return the page ID for an existing WikiJS page, or None if not found."""
|
||||||
|
query = """
|
||||||
|
query ($path: String!, $locale: String!) {
|
||||||
|
pages {
|
||||||
|
singleByPath(path: $path, locale: $locale) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = gql(query, {"path": wiki_path, "locale": LOCALE})
|
||||||
|
page = data["data"]["pages"]["singleByPath"]
|
||||||
|
return page["id"] if page else None
|
||||||
|
except RuntimeError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def derive_title(local_path: str) -> str:
|
||||||
|
stem = Path(local_path).stem # e.g. "ignition-api"
|
||||||
|
if stem.upper() == "README":
|
||||||
|
# Use parent directory name for README files
|
||||||
|
parent = Path(local_path).parent.name
|
||||||
|
if parent == ".":
|
||||||
|
return "Home"
|
||||||
|
return parent.replace("-", " ").replace("_", " ").title()
|
||||||
|
return stem.replace("-", " ").replace("_", " ").title()
|
||||||
|
|
||||||
|
|
||||||
|
def create_page(wiki_path: str, title: str, content: str) -> None:
|
||||||
|
mutation = """
|
||||||
|
mutation ($path: String!, $title: String!, $content: String!, $locale: String!) {
|
||||||
|
pages {
|
||||||
|
create(
|
||||||
|
path: $path
|
||||||
|
title: $title
|
||||||
|
content: $content
|
||||||
|
locale: $locale
|
||||||
|
editor: "markdown"
|
||||||
|
isPublished: true
|
||||||
|
isPrivate: false
|
||||||
|
tags: []
|
||||||
|
description: ""
|
||||||
|
) {
|
||||||
|
responseResult {
|
||||||
|
succeeded
|
||||||
|
errorCode
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
data = gql(mutation, {"path": wiki_path, "title": title, "content": content, "locale": LOCALE})
|
||||||
|
result = data["data"]["pages"]["create"]["responseResult"]
|
||||||
|
if not result["succeeded"]:
|
||||||
|
raise RuntimeError(f"Create failed [{result['errorCode']}]: {result['message']}")
|
||||||
|
|
||||||
|
|
||||||
|
def update_page(page_id: int, title: str, content: str) -> None:
|
||||||
|
mutation = """
|
||||||
|
mutation ($id: Int!, $title: String!, $content: String!) {
|
||||||
|
pages {
|
||||||
|
update(
|
||||||
|
id: $id
|
||||||
|
title: $title
|
||||||
|
content: $content
|
||||||
|
editor: "markdown"
|
||||||
|
isPublished: true
|
||||||
|
isPrivate: false
|
||||||
|
tags: []
|
||||||
|
description: ""
|
||||||
|
) {
|
||||||
|
responseResult {
|
||||||
|
succeeded
|
||||||
|
errorCode
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
data = gql(mutation, {"id": page_id, "title": title, "content": content})
|
||||||
|
result = data["data"]["pages"]["update"]["responseResult"]
|
||||||
|
if not result["succeeded"]:
|
||||||
|
raise RuntimeError(f"Update failed [{result['errorCode']}]: {result['message']}")
|
||||||
|
|
||||||
|
|
||||||
|
# ── Main ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Sync markdown files to WikiJS")
|
||||||
|
parser.add_argument("--dry-run", action="store_true", help="Preview only, no API calls")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.dry_run:
|
||||||
|
print("Dry run — no changes will be made.\n")
|
||||||
|
|
||||||
|
errors = 0
|
||||||
|
|
||||||
|
for local_rel, wiki_path in MANIFEST.items():
|
||||||
|
local_file = REPO_ROOT / local_rel
|
||||||
|
title = derive_title(local_rel)
|
||||||
|
|
||||||
|
if not local_file.exists():
|
||||||
|
print(f"[SKIP] {local_rel} (file not found)")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if args.dry_run:
|
||||||
|
print(f"[DRY-RUN] {local_rel} → {WIKIJS_URL}/{wiki_path} (title: {title!r})")
|
||||||
|
continue
|
||||||
|
|
||||||
|
content = local_file.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
try:
|
||||||
|
page_id = page_id_for_path(wiki_path)
|
||||||
|
if page_id is None:
|
||||||
|
create_page(wiki_path, title, content)
|
||||||
|
print(f"[CREATED] {local_rel} → /{wiki_path}")
|
||||||
|
else:
|
||||||
|
update_page(page_id, title, content)
|
||||||
|
print(f"[OK] {local_rel} → /{wiki_path}")
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"[ERROR] {local_rel}: {exc}", file=sys.stderr)
|
||||||
|
errors += 1
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
print(f"\n{errors} file(s) failed.", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in New Issue
Block a user