sylo-ignition 0.1.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.
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env python3
2
+ """Offline lint/validate of a project resource before scanning.
3
+
4
+ For ignition_validate tool. Heuristic (no vendor SDK):
5
+ - view.json: structure, unique meta.name, known component types, binding shapes
6
+ - resource.json: required keys
7
+ - .py (project scripts): Jython 2.7 compatibility (f-strings etc. are errors)
8
+ - tag JSON: light structure check
9
+ Errors block the write; warnings are advisory. This tool never calls the gateway.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import json
16
+ import re
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ from _ignition import load_config, project_dir, resolve_project
21
+ from _json_out import emit, emit_error
22
+
23
+ KNOWN_VIEW_TYPES = {
24
+ "ia.display.label",
25
+ "ia.display.led",
26
+ "ia.display.multistate-indicator",
27
+ "ia.display.table",
28
+ "ia.display.path",
29
+ "ia.shapes.svg",
30
+ "ia.input.btn",
31
+ "ia.input.dropdown",
32
+ "ia.input.numeric-entry",
33
+ "ia.input.text-field",
34
+ "ia.input.slider",
35
+ "ia.input.toggle-switch",
36
+ "ia.input.checkbox",
37
+ "ia.container.flex",
38
+ "ia.container.column",
39
+ "ia.container.row",
40
+ "ia.container.coord",
41
+ "ia.container.tabs",
42
+ "ia.container.accordian",
43
+ "ia.container.dropdown-menu",
44
+ "ia.container.menus",
45
+ "ia.container.modal",
46
+ "ia.container.popup",
47
+ "ia.container.split",
48
+ "ia.container.tab-container",
49
+ "ia.container.template",
50
+ "ia.chart.xy-chart",
51
+ "ia.powerchart.powerchart",
52
+ "ia.gauge.led-numeric-entry",
53
+ "ia.gauge.linear-gauge",
54
+ "ia.gauge.radial-gauge",
55
+ "ia.gauge.arc",
56
+ "ia.alarm.journal-table",
57
+ "ia.alarm.status-table",
58
+ "ia.logos.inductive-automation",
59
+ "ia.display.image",
60
+ "ia.display.markdown",
61
+ "ia.embedded.websocket",
62
+ "ia.embedded.video",
63
+ "ia.barcode.qrcode",
64
+ "ia.tagtags.tag-canvas",
65
+ "em.charts.chartjs",
66
+ }
67
+ BINDING_TYPES = {"property", "tag", "expr", "query", "tagHistory", "propertyBinding", "exprBinding", "tagBinding"}
68
+
69
+
70
+ def main() -> None:
71
+ parser = argparse.ArgumentParser()
72
+ parser.add_argument("--project", default="")
73
+ parser.add_argument("--path", required=True)
74
+ args = parser.parse_args()
75
+
76
+ cfg = load_config(require_token=False)
77
+ name = resolve_project(cfg, args.project)
78
+ root = project_dir(cfg, name)
79
+ target = (root / args.path.replace("\\", "/").strip("/")).resolve()
80
+ if not target.is_file():
81
+ emit_error(f"Not found: {args.path} (in {root})")
82
+
83
+ errors: list[str] = []
84
+ warnings: list[str] = []
85
+ checks: list[str] = []
86
+
87
+ raw = target.read_text(encoding="utf-8", errors="replace")
88
+
89
+ if target.name == "view.json":
90
+ validate_view(raw, errors, warnings, checks)
91
+ elif target.name == "resource.json":
92
+ validate_resource_json(raw, errors, warnings, checks)
93
+ elif target.suffix == ".py":
94
+ validate_jython(raw, errors, warnings, checks)
95
+ elif target.suffix == ".json":
96
+ checks.append("generic JSON parse")
97
+ try:
98
+ json.loads(raw)
99
+ except (json.JSONDecodeError, ValueError) as e:
100
+ errors.append(f"invalid JSON: {e}")
101
+ else:
102
+ checks.append("no validation rules for this file type — skipped")
103
+
104
+ emit(
105
+ {
106
+ "ok": len(errors) == 0,
107
+ "project": name,
108
+ "path": args.path,
109
+ "errors": errors,
110
+ "warnings": warnings,
111
+ "checks_run": checks,
112
+ }
113
+ )
114
+
115
+
116
+ # ---- view.json ----------------------------------------------------------------
117
+
118
+ def validate_view(raw: str, errors: list[str], warnings: list[str], checks: list[str]) -> None:
119
+ checks.append("view.json: JSON parse")
120
+ try:
121
+ view = json.loads(raw)
122
+ except (json.JSONDecodeError, ValueError) as e:
123
+ errors.append(f"view.json is not valid JSON: {e}")
124
+ return
125
+ if not isinstance(view, dict):
126
+ errors.append("view.json root must be an object")
127
+ return
128
+ checks.append("view.json: root component (verified 8.3.9 shape)")
129
+
130
+ # VERIFIED structure (live 8.3.9): top-level "root" is the root COMPONENT
131
+ # (e.g. ia.container.coord) with children — NOT props.root.children.
132
+ # A malformed view is silently dropped by the scan.
133
+ root = view.get("root")
134
+ if not isinstance(root, dict):
135
+ errors.append(
136
+ 'view.json must have a top-level "root" component object '
137
+ '(e.g. {"type": "ia.container.coord", "meta": {"name": "root"}, "children": [...]})'
138
+ )
139
+ return
140
+ if root.get("type") not in ("ia.container.coord", "ia.container.flex", "ia.container.column", "ia.container.row"):
141
+ errors.append(f'root.type should be a container type, got {root.get("type")!r}')
142
+ if not isinstance(root.get("children"), list):
143
+ warnings.append("root.children missing or not a list — empty view?")
144
+
145
+ names: list[str] = []
146
+ depth_violations: list[str] = []
147
+ count = 0
148
+ percent_mode_nodes: list[str] = []
149
+
150
+ def walk(node: Any, path: str, depth: int) -> None:
151
+ nonlocal count
152
+ if not isinstance(node, dict):
153
+ return
154
+ count += 1
155
+ ctype = node.get("type", "")
156
+ if isinstance(ctype, str) and ctype and ctype not in KNOWN_VIEW_TYPES and not ctype.startswith("ia."):
157
+ warnings.append(f"unknown component type {ctype!r} at {path}")
158
+ meta = node.get("meta", {})
159
+ nm = meta.get("name") if isinstance(meta, dict) else None
160
+ if nm:
161
+ if nm in names:
162
+ errors.append(f"duplicate meta.name {nm!r} at {path} — names must be unique")
163
+ names.append(nm)
164
+ pos = node.get("position")
165
+ if isinstance(pos, dict) and pos.get("mode") == "percent":
166
+ percent_mode_nodes.append(path)
167
+ if depth > 12:
168
+ depth_violations.append(path)
169
+ for child in node.get("children", []) or []:
170
+ walk(child, f"{path}/{nm or ctype}", depth + 1)
171
+
172
+ for child in root.get("children") or []:
173
+ walk(child, "<root>", 1)
174
+ checks.append(f"view.json: {count} components, {len(names)} named")
175
+
176
+ if depth_violations:
177
+ warnings.append(f"deep nesting (>12) at: {', '.join(depth_violations[:5])}")
178
+ if count > 300:
179
+ warnings.append(f"{count} components — very heavy view; consider templates/splitting")
180
+ if percent_mode_nodes:
181
+ warnings.append(
182
+ "position.mode 'percent' observed but UNVERIFIED on 8.3.9 (labels stacked at 0,0 in "
183
+ "live testing — use absolute px inside coord containers until verified)"
184
+ )
185
+
186
+ # Binding shape check: any {binding: {...}} must have a known type
187
+ bind_re = re.compile(r'"binding"\s*:\s*\{')
188
+ if bind_re.search(raw):
189
+ checks.append("view.json: binding objects present — verifying types")
190
+ bad = find_bad_bindings(view)
191
+ for b in bad:
192
+ errors.append(f"binding without a known 'type' at {b}")
193
+
194
+ # Perspective expression binding syntax quick check
195
+ for m in re.finditer(r"\{([^{}\n]{1,200})\}", raw):
196
+ expr = m.group(1)
197
+ if re.search(r"\bf\"|f'", expr):
198
+ warnings.append(f"possible f-string in expression {expr[:60]!r} — Perspective expressions are not Python")
199
+
200
+
201
+ def find_bad_bindings(node: Any, path: str = "") -> list[str]:
202
+ bad: list[str] = []
203
+ if isinstance(node, dict):
204
+ if set(node.keys()) == {"binding"} or "binding" in node:
205
+ b = node.get("binding")
206
+ if isinstance(b, dict) and b.get("type") not in BINDING_TYPES:
207
+ bad.append(path or "<root>")
208
+ for k, v in node.items():
209
+ bad.extend(find_bad_bindings(v, f"{path}/{k}"))
210
+ elif isinstance(node, list):
211
+ for i, v in enumerate(node):
212
+ bad.extend(find_bad_bindings(v, f"{path}[{i}]"))
213
+ return bad
214
+
215
+
216
+ # ---- resource.json ------------------------------------------------------------
217
+
218
+ def validate_resource_json(raw: str, errors: list[str], warnings: list[str], checks: list[str]) -> None:
219
+ checks.append("resource.json: keys")
220
+ try:
221
+ meta = json.loads(raw)
222
+ except (json.JSONDecodeError, ValueError) as e:
223
+ errors.append(f"resource.json is not valid JSON: {e}")
224
+ return
225
+ for key in ("scope", "version", "files"):
226
+ if key not in meta:
227
+ errors.append(f"resource.json missing key: {key}")
228
+ if meta.get("scope") != "G":
229
+ errors.append("resource.json scope must be 'G'")
230
+
231
+
232
+ # ---- Jython 2.7 ---------------------------------------------------------------
233
+
234
+ def validate_jython(raw: str, errors: list[str], warnings: list[str], checks: list[str]) -> None:
235
+ checks.append("python: Jython 2.7 compatibility")
236
+ for i, line in enumerate(raw.splitlines(), 1):
237
+ if re.search(r"\bf['\"]", line):
238
+ errors.append(f"line {i}: f-string — Jython 2.7 has no f-strings; use .format() or %")
239
+ if re.search(r"\basync\s+def\b|\bawait\b", line):
240
+ errors.append(f"line {i}: async/await — not available in Jython 2.7")
241
+ if ":=" in line and not line.strip().startswith("#"):
242
+ warnings.append(f"line {i}: walrus ':=' — not available in Jython 2.7")
243
+ if re.match(r"\s*print\s+[^(\s]", line) and "#" not in line.split("print")[0]:
244
+ warnings.append(f"line {i}: print statement without parens (works in 2.7, not Python 3)")
245
+ if re.search(r"\bmatch\s+\w+.*:$", line.strip()) and "case" in raw:
246
+ warnings.append(f"line {i}: 'match' statement — Jython 2.7 has no structural pattern matching")
247
+ if re.search(r"^\s*import\s+(dataclasses|typing\.extensions|pathlib)\b", raw, re.M):
248
+ warnings.append("import of a Python-3-only stdlib module (dataclasses/typing.extensions/pathlib) — unavailable in Jython 2.7")
249
+
250
+
251
+ if __name__ == "__main__":
252
+ main()
@@ -0,0 +1,131 @@
1
+ ---
2
+ name: ignition
3
+ description: Create and edit Ignition 8.3 projects — file-based resource authoring on the gateway's data dir, REST scan to hot-apply, screenshot-verified Perspective UI. Write-allowlist gated; 8.1 is NOT supported.
4
+ metadata:
5
+ sylo:
6
+ category: automation
7
+ icon: cpu
8
+ ---
9
+
10
+ # Ignition — file-based 8.3 workflow
11
+
12
+ Sylo edits Ignition **8.3** project resources directly on disk (`data/projects/<name>/...`),
13
+ then triggers a gateway **scan** to hot-apply them — no Designer import step, no zip. All
14
+ mutating tools enforce the operator-managed **write-allowlist**. This targets **8.3 only**:
15
+ 8.1 keeps projects + config in an internal SQLite DB with no REST API — see the reference
16
+ skill's version notes.
17
+
18
+ ## First-run setup (operator, one time)
19
+
20
+ 1. **API key**: gateway web UI → Platform → Security → API Keys → Create API Key +
21
+ (Basic Token). Uncheck "Require secure connections" for a plain-HTTP local gateway.
22
+ Copy the token.
23
+ 2. **Security level**: Platform → Security → **Levels** → select `Authenticated` →
24
+ Add Level + → name it `SyloAPI` → Save. Then edit the API key and check `SyloAPI`.
25
+ (Role-derived levels like Administrator are greyed out for API keys — custom levels are not.)
26
+ 3. **Grant access**: Platform → Security → General Settings → **Roles and Permissions** →
27
+ check `SyloAPI` for Gateway Read AND Gateway Write. Save.
28
+ 4. **Config file** at `C:\Users\<user>\.ignition-sylo\config.json` (OUTSIDE any repo —
29
+ tokens never go in git):
30
+ ```json
31
+ {
32
+ "gateway_url": "http://localhost:8088",
33
+ "api_token": "Sylo:...",
34
+ "data_dir": "C:\\Program Files\\Inductive Automation\\Ignition\\data",
35
+ "default_project": "SyloSandbox"
36
+ }
37
+ ```
38
+ 5. **File-write permission** (native Windows install under Program Files — one time,
39
+ elevated PowerShell; the Users group only has read on the data dir by default):
40
+ ```powershell
41
+ icacls "C:\Program Files\Inductive Automation\Ignition\data\projects" /grant "<user>:(OI)(CI)M"
42
+ icacls "C:\Program Files\Inductive Automation\Ignition\data\config" /grant "<user>:(OI)(CI)M"
43
+ ```
44
+ 6. `ignition_status` should now show `gateway_reachable: true` and the project list.
45
+
46
+ ## Core loop
47
+
48
+ 1. **`ignition_status`** — always first. Note open Designer sessions (conflict risk),
49
+ which projects exist, and what the allowlist permits.
50
+ 2. **`ignition_project_resources`** — see the resource tree of the project you'll touch.
51
+ 3. **`ignition_resource_read`** — study existing views before authoring (match the
52
+ house style; the Example project's `mainView` is a good reference).
53
+ 4. Author/edit → **`ignition_validate`** (catch structure + Jython-2.7 errors offline).
54
+ 5. **`ignition_resource_write`** — allowlist-gated atomic write. New folders get a
55
+ scan-compatible `resource.json` scaffold automatically.
56
+ 6. **`ignition_scan`** (scope projects) — hot-apply into the gateway + Designer.
57
+ 7. **`ignition_screenshot`** → `analyze_image` — visual verification / design critique loop.
58
+ 8. If something didn't apply: **`ignition_gateway_logs`** (search "scan" / "resource").
59
+
60
+ Use **`ignition_project_create`** for a scratch project, and **`ignition_backup`**
61
+ before risky or bulk changes (restorable .gwbk in `~/.ignition-sylo/backups/`).
62
+ **`ignition_api_get`** is the read-only passthrough for everything else on the
63
+ 588-route API (resource lists, sessions, tag export JSON, gateway info...).
64
+
65
+ ## Gates (hard rules)
66
+
67
+ | Tool | Gate |
68
+ |------|------|
69
+ | `ignition_resource_write` | project must be present + **enabled** in `assets/write-allowlist.json` |
70
+ | `ignition_scan` | `allow_scan` must be true |
71
+ | `ignition_project_create` | `allow_project_create` must be true (creates only NEW isolated projects) |
72
+ | write-allowlist itself | **never edit it as the agent** — operator-owned; ask the operator |
73
+
74
+ If a gate blocks you, explain to the operator exactly what to change and stop.
75
+ Default scratch project: **SyloSandbox** (pre-enabled). Never write `Example`
76
+ (read-only by default) or any production project the operator hasn't listed.
77
+
78
+ ## Forbidden paths (the write tool also refuses these)
79
+
80
+ - `**/.resources/`, `*.digest.json` — gateway internals
81
+ - `data/var/**`, `config/local/**`, `*.idb` — runtime state / local overrides
82
+ - `*.bin`, thumbnails, images — binary payloads produced by Designer/gateway
83
+ - Existing `resource.json` — the gateway owns it (signatures/timestamps);
84
+ new-resource scaffolds are created automatically by the write tool
85
+ - Gateway `data/config/**` — out of scope for project writes (Phase 3)
86
+
87
+ ## Designer conflict rule
88
+
89
+ A scan while the Designer has **unsaved edits** can clobber them. Before
90
+ `ignition_scan`, check `ignition_status`. If Designer sessions are open, ask the
91
+ operator to save/close (or confirm proceed). Never scan silently over an open
92
+ Designer.
93
+
94
+ ## Gotchas
95
+
96
+ - **Views are reachable only via pages**: a new project has no page-config, so sessions
97
+ show "No view configured for this page". Write
98
+ `com.inductiveautomation.perspective/page-config/config.json` mapping `"/"` to your
99
+ view path (schema in the reference skill), then scan. The URL after the project name
100
+ is the PAGE path, not the view path.
101
+ - **The scan silently drops malformed resources** — a bad view.json produces no error,
102
+ just an absent view. ALWAYS screenshot-verify after scanning; if the view is missing,
103
+ check the structure against the verified schema (top-level `root` component — see
104
+ reference skill) and `ignition_gateway_logs`.
105
+ - **Position modes**: `flow` inside flex containers is verified working; **`percent`
106
+ inside coord containers did NOT apply in live testing** (labels stacked at 0,0) — use
107
+ absolute pixel x/y/width/height inside coord containers.
108
+ - **Jython 2.7**: project gateway scripts are Python 2 — no f-strings, no
109
+ async/await, no pathlib. `ignition_validate` catches these; write `.format()` code.
110
+ - **Windows paths**: resource paths use forward slashes in tool calls; the
111
+ scripts handle OS joins. Keep view folder names short (255-char path limit).
112
+ - **Program Files ACL**: native installs need the one-time icacls grant (setup step 5)
113
+ — otherwise writes fail with Access denied.
114
+ - **Encoding**: scripts run with UTF-8 forced; write tool always writes UTF-8
115
+ with `\n` endings.
116
+ - **Scans are global**: `scan/projects` scans ALL projects, not just yours —
117
+ another reason the Designer rule matters.
118
+ - **No tag VALUES via REST**: 8.3 REST manages tag *definitions* (tags export/import
119
+ JSON); live values go through scripting/OPC-UA — currently out of scope.
120
+ - The full offline 8.3 User Manual (1,566 pages) + SDK docs live in the Sylo repo at
121
+ `packages/sylo-ignition/references/` — grep them via the **ignition-reference** skill
122
+ before guessing formats.
123
+
124
+ ## Design quality (operator standard: beautiful, intuitive, clean)
125
+
126
+ When building views, aim for real design quality, not just function: consistent
127
+ spacing rhythm, meaningful hierarchy (page title → section → content), color used
128
+ sparingly for state (not decoration), labels that read like the machine ("Conveyor
129
+ Speed" not "tag1"), large readable values for at-a-glance operation. Always
130
+ screenshot and critique against these rules before reporting done; iterate at
131
+ least once when something feels off.
@@ -0,0 +1,157 @@
1
+ ---
2
+ name: ignition-reference
3
+ description: Ignition 8.3 offline documentation map + format quickrefs — Perspective view JSON, bindings, component catalog, tag JSON, theme files, gateway REST routes, Jython 2.7. Grep the local manual before guessing.
4
+ metadata:
5
+ sylo:
6
+ category: automation
7
+ icon: cpu
8
+ ---
9
+
10
+ # Ignition reference — offline docs + format quickrefs
11
+
12
+ ## Offline documentation (grep here first)
13
+
14
+ Full 8.3 User Manual as grep-friendly Markdown (1,566 pages) + SDK guide live in the
15
+ Sylo repo:
16
+
17
+ - `packages/sylo-ignition/references/user-manual-8.3/` — the whole manual, Docusaurus
18
+ pages converted to `.md` (pipe/grid tables, fenced code, internal links rewritten
19
+ to relative `.md`). `references/README.md` lists key pages.
20
+ - `packages/sylo-ignition/references/sdk-docs/` — SDK Programmer's Guide (resource
21
+ collections, module model).
22
+ - `packages/sylo-ignition/references/user-manual-8.1-pdfs/` — official 8.1 PDFs
23
+ (git-ignored, 262 MB; 8.1 has NO REST API / file config — only zip + gwbk workflows).
24
+ - `packages/sylo-ignition/references/gateway-rest-api-8.3.md` — verified route map of
25
+ the 588-route REST API + the working API-key auth recipe.
26
+ - Regenerate everything: `python packages/sylo-ignition/scripts/fetch_docs.py`.
27
+
28
+ Key manual pages (relative to `user-manual-8.3/`):
29
+
30
+ - Perspective components: `appendix/components/perspective-components/` (every
31
+ component: props, events, style tips)
32
+ - Perspective props/bindings: `perspective/props/` and `perspective/bindings/`
33
+ - Expressions: `appendix/expression-functions/` ( Perspective expression language)
34
+ - Scripting (Jython 2.7): `appendix/scripting-functions/` + `perspective/scripting/`
35
+ - Tags: `platform/tags/` (tag properties, UDTs, tag file JSON)
36
+ - Gateway folder structure: `appendix/reference-pages/gateway-folder-structure.md`
37
+ - Version control / file-based config: `tutorials/version-control-guide.md`
38
+ - Security: `platform/security/` (API keys, security levels, zones)
39
+
40
+ ## On-disk layout (8.3, verified on a live gateway)
41
+
42
+ ```
43
+ data/
44
+ ├── projects/<Name>/
45
+ │ ├── project.json # {title, description, enabled, inheritable, parent}
46
+ │ └── com.inductiveautomation.perspective/
47
+ │ ├── views/<viewPath>/ # view.json + thumbnail.png + resource.json
48
+ │ ├── page-config/config.json/
49
+ │ ├── page-startup/onPageStartup.py/
50
+ │ └── session-props/props.json/
51
+ └── config/ # gateway config (JSON, scan via /scan/config)
52
+ ```
53
+
54
+ `resource.json` (per folder, gateway-owned — never hand-edit existing):
55
+ `{scope: "G", version: 1, restricted, overridable, files: [...], attributes:
56
+ {lastModificationSignature, lastModification: {actor, timestamp}}}`
57
+
58
+ ## Perspective view.json quickref (VERIFIED live on 8.3.9)
59
+
60
+ ```json
61
+ {
62
+ "custom": {},
63
+ "params": {},
64
+ "props": {},
65
+ "root": {
66
+ "type": "ia.container.coord",
67
+ "meta": { "name": "root" },
68
+ "children": [
69
+ {
70
+ "type": "ia.display.label",
71
+ "meta": { "name": "titleLabel" },
72
+ "position": { "x": 24, "y": 24, "width": 400, "height": 40 },
73
+ "props": {
74
+ "text": "Machine Overview",
75
+ "style": { "fontSize": "24px", "fontWeight": "bold" }
76
+ }
77
+ }
78
+ ]
79
+ }
80
+ }
81
+ ```
82
+
83
+ - **`root` is a top-level key holding the root COMPONENT** (usually
84
+ `ia.container.coord`) — NOT `props.root.children`. Malformed views are
85
+ silently dropped by the scan.
86
+ - Component `type` comes from the catalog (`ia.<family>.<component>`); full prop
87
+ details in `appendix/components/perspective-components/`.
88
+ - `meta.name` must be **unique** across the view.
89
+ - Position: absolute px (`x`,`y`,`width`,`height`) inside coord containers — VERIFIED.
90
+ `mode: "flow"` for children of flex containers — VERIFIED. `mode: "percent"` —
91
+ did NOT apply in live testing (unverified; avoid for now).
92
+ - Style lives under `props.style` (CSS-ish: fontSize, backgroundColor, padding,
93
+ width, height); shared looks go in **style classes** (manual: `perspective/style-classes/`),
94
+ referenced via `props.style.classes`.
95
+
96
+ ## Page-config quickref (VERIFIED live on 8.3.9)
97
+
98
+ `com.inductiveautomation.perspective/page-config/config.json` — without it,
99
+ sessions show "No view configured for this page":
100
+
101
+ ```json
102
+ {
103
+ "pages": {
104
+ "/": { "viewPath": "Home" },
105
+ "/alarms": { "viewPath": "Alarms/Console" }
106
+ },
107
+ "sharedDocks": { "cornerPriority": "top-bottom" }
108
+ }
109
+ ```
110
+
111
+ URL `…/data/perspective/client/<project>/<page>` maps `page` to the pages key above
112
+ (root page = `/`).
113
+
114
+ ## Bindings quickref
115
+
116
+ Property bindings wrap the value:
117
+
118
+ ```json
119
+ "props": { "text": { "binding": { "type": "property", "config": { "path": "view.params.title" } } } }
120
+ ```
121
+
122
+ - `type: "property"` → `config.path` (`view.x`, `self.x`, `session.props.x`)
123
+ - `type: "tag"` → `config.path: "[default]Line1/Speed"`, optional `config.op` e.g.
124
+ `readBlocking`? (see manual `perspective/bindings/tag-binding.md` for transforms/format)
125
+ - `type: "expr"` → `config.expression` — Perspective expression language
126
+ (`{view.params.speed} * 60`, `coalesce()`, `if()`, `toFix()`)
127
+ - Transforms: `config.transforms: [{"type": "map", "config": {...}}]` etc.
128
+
129
+ ## Tag file JSON quickref
130
+
131
+ Tags (definitions) live in the provider's config or can be round-tripped as JSON via
132
+ `GET /data/api/v1/tags/export` / `POST /data/api/v1/tags/import`. UDT definitions,
133
+ value types (`Int4`, `Float8`, `Boolean`, `String8`), alarm definitions — see
134
+ `platform/tags/tag-properties.md` and `platform/tags/udts/`. REST manages
135
+ definitions; live **values** need scripting/OPC-UA.
136
+
137
+ ## Jython 2.7 quickref (gateway scripts)
138
+
139
+ Python 2 syntax: `print x` ok, prefer `print(x)`; string formatting `%` or
140
+ `.format()`; no f-strings, no async, no pathlib/dataclasses/match. `system.*`
141
+ scripting functions in `appendix/scripting-functions/` (e.g.
142
+ `system.tag.readBlocking`, `system.perspective.sendMessage`).
143
+
144
+ ## Gateway REST quickref
145
+
146
+ - Auth: header `X-Ignition-API-Token`; 401 = dead token, 403 = insufficient
147
+ security level (see gateway-rest-api-8.3.md for the working SyloAPI recipe)
148
+ - Projects: `/data/api/v1/projects/{list,names,find/{n},copy,rename/{n},export/{n},import/{n}}`
149
+ - Scan: `POST /data/api/v1/scan/{projects,config}`; lock:
150
+ `POST /data/api/v1/scan-lock/projects`
151
+ - Resources (gateway config types incl. tag-provider, opcua device, perspective
152
+ themes): `/data/api/v1/resources/{list,names,find,singleton}/{moduleId}/{typeId}`
153
+ - Tags: `/data/api/v1/tags/{export,import}` (JSON definitions)
154
+ - Perspective: `/data/perspective/api/v1/sessions/`, `.../themes/copy-base-themes`
155
+ - Status/diagnostics: `/data/api/v1/{gateway-info,overview,overview/problems,logs}`,
156
+ `GET /data/api/v1/backup` (.gwbk), `/data/api/v1/designers` (open sessions)
157
+ - Full spec: `GET /openapi.json` (12 MB — save to disk, don't inline)